diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index 6b119cd..de8bb77 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -22,7 +22,7 @@ struct Example { // Using the new extension method that doesn't require type hints let privateKey = P256.Signing.PrivateKey() - try await Server.serve( + let server = NIOHTTPServer>( logger: logger, configuration: .init( bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345), @@ -43,18 +43,12 @@ struct Example { ], privateKey: Certificate.PrivateKey(privateKey) ) - ), handler: handler(request:requestConcludingAsyncReader:responseSender:)) - } - - // This is a workaround for a current bug with the compiler. - @Sendable - nonisolated(nonsending) private static func handler( - request: HTTPRequest, - requestConcludingAsyncReader: consuming HTTPRequestConcludingAsyncReader, - responseSender: consuming HTTPResponseSender - ) async throws { - let writer = try await responseSender.sendResponse(HTTPResponse(status: .ok)) - try await writer.writeAndConclude(element: "Well, hello!".utf8.span, finalElement: nil) + ) + ) + try await server.serve { request, requestBodyAndTrailers, responseSender in + let writer = try await responseSender.send(HTTPResponse(status: .ok)) + try await writer.writeAndConclude(element: "Well, hello!".utf8.span, finalElement: nil) + } } } @@ -63,12 +57,10 @@ struct Example { // 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 Server { +extension NIOHTTPServer where RequestHandler == HTTPServerClosureRequestHandler { /// Serve HTTP requests using a middleware chain built with the provided builder /// This method handles the type inference for HTTP middleware components - static func serve( - logger: Logger, - configuration: HTTPServerConfiguration, + func serve( @MiddlewareChainBuilder withMiddleware middlewareBuilder: () -> some Middleware< RequestResponseMiddlewareBox< @@ -77,13 +69,10 @@ extension Server { >, Never > & Sendable - ) async throws where RequestHandler == HTTPServerClosureRequestHandler { + ) async throws { let chain = middlewareBuilder() - try await serve( - logger: logger, - configuration: configuration - ) { request, reader, responseSender in + try await self.serve { request, reader, responseSender in try await chain.intercept(input: RequestResponseMiddlewareBox( request: request, requestReader: reader, diff --git a/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift b/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift index 81d75ff..9617d92 100644 --- a/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift +++ b/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift @@ -50,7 +50,8 @@ where requestReader: wrappedReader, responseSender: HTTPResponseSender { [logger] response in if let sender = maybeSender.take() { - let writer = try await sender.sendResponse(response) + logger.info("Sending response \(response)") + let writer = try await sender.send(response) return HTTPResponseLoggingConcludingAsyncWriter( base: writer, logger: logger @@ -58,6 +59,9 @@ where } else { fatalError("Called closure more than once") } + } sendInformational: { response in + self.logger.info("Sending informational response \(response)") + try await maybeSender?.sendInformational(response) } ) try await next(requestResponseBox) diff --git a/Sources/Example/Middlewares/RouteHandlerMiddleware.swift b/Sources/Example/Middlewares/RouteHandlerMiddleware.swift index 4380387..d4b6494 100644 --- a/Sources/Example/Middlewares/RouteHandlerMiddleware.swift +++ b/Sources/Example/Middlewares/RouteHandlerMiddleware.swift @@ -22,7 +22,7 @@ where ) async throws { try await input.withContents { request, requestReader, responseSender in var maybeReader = Optional(requestReader) - try await responseSender.sendResponse(HTTPResponse(status: .accepted)) + try await responseSender.send(HTTPResponse(status: .accepted)) .produceAndConclude { responseBodyAsyncWriter in var responseBodyAsyncWriter = responseBodyAsyncWriter if let reader = maybeReader.take() { diff --git a/Sources/HTTPServer/HTTPResponseSender.swift b/Sources/HTTPServer/HTTPResponseSender.swift index 488cf2c..eafa4a4 100644 --- a/Sources/HTTPServer/HTTPResponseSender.swift +++ b/Sources/HTTPServer/HTTPResponseSender.swift @@ -1,25 +1,41 @@ public import HTTPTypes -/// This type ensures that a single `HTTPResponse` is sent back to the client when handling a request with -/// ``Server/serve(logger:configuration:handler:)-(_,_,RequestHandler)`` or ``Server/serve(logger:configuration:handler:)-(_,_,(HTTPRequest,HTTPRequestConcludingAsyncReader,HTTPResponseSender)->Void)``. +/// 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 the handler, and they will only be allowed to call ``sendResponse(_:)`` -/// once before the sender is consumed and cannot be referenced again. This forces structure in the response flow, requiring users to -/// send a single response before they can stream a response body and trailers using the returned `ResponseWriter`. +/// The user will get a ``HTTPResponseSender`` as part of +/// ``HTTPServerRequestHandler/handle(request: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. +/// +/// This forces structure in the response flow, requiring users to send a single response before they can stream a response body and +/// trailers using the returned `ResponseWriter`. public struct HTTPResponseSender: ~Copyable { - private let _sendResponse: (HTTPResponse) async throws -> ResponseWriter + private let _sendInformational: (HTTPResponse) async throws -> Void + private let _send: (HTTPResponse) async throws -> ResponseWriter public init( - _ sendResponse: @escaping (HTTPResponse) async throws -> ResponseWriter + send: @escaping (HTTPResponse) async throws -> ResponseWriter, + sendInformational: @escaping (HTTPResponse) async throws -> Void ) { - self._sendResponse = sendResponse + self._send = send + self._sendInformational = sendInformational } /// Send the given `HTTPResponse` and get back a `ResponseWriter` to which to write a response body and trailers. - /// - Parameter response: The `HTTPResponse` to send back to the client. + /// - Parameter response: The final `HTTPResponse` to send back to the client. /// - Returns: The `ResponseWriter` to which to write a response body and trailers. - consuming public func sendResponse(_ response: HTTPResponse) async throws -> ResponseWriter { - try await self._sendResponse(response) + /// - Important: Note this method is consuming: after you send this response, you won't be able to send any more responses. + /// If you need to send an informational (1xx) response, use ``sendInformational(_:)`` instead. + consuming public func send(_ response: HTTPResponse) async throws -> ResponseWriter { + precondition(response.status.kind != .informational) + return try await self._send(response) + } + + /// Send the given informational (1xx) response. + /// - Parameter response: An informational `HTTPResponse` to send back to the client. + public func sendInformational(_ response: HTTPResponse) async throws { + precondition(response.status.kind == .informational) + return try await _sendInformational(response) } } diff --git a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift index b677191..07b09bf 100644 --- a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift @@ -24,7 +24,7 @@ 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: HTTPServerRequestHandler { /// The underlying closure that handles HTTP requests private let _handler: nonisolated(nonsending) @Sendable ( @@ -36,7 +36,7 @@ public struct HTTPServerClosureRequestHandler: HTTPServerRequestHandler { /// Creates a new closure-based HTTP request handler. /// /// - Parameter handler: A closure that will be called to handle each incoming HTTP request. - /// The closure takes the same parameters as the ``HTTPServerRequestHandler/handle(request:requestConcludingAsyncReader:sendResponse:)`` method. + /// The closure takes the same parameters as the ``HTTPServerRequestHandler/handle(request:requestBodyAndTrailers:responseSender:)`` method. public init( handler: nonisolated(nonsending) @Sendable @escaping ( HTTPRequest, @@ -53,13 +53,13 @@ public struct HTTPServerClosureRequestHandler: HTTPServerRequestHandler { /// /// - Parameters: /// - request: The HTTP request headers and metadata. - /// - requestConcludingAsyncReader: A reader for accessing the request body data and trailing headers. - /// - sendResponse: A callback function to send the HTTP response. + /// - requestBodyAndTrailers: A reader for accessing the request body data and trailing headers. + /// - responseSender: An ``HTTPResponseSender`` to send the HTTP response. public func handle( request: HTTPRequest, - requestConcludingAsyncReader: consuming HTTPRequestConcludingAsyncReader, - sendResponse: consuming HTTPResponseSender + requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader, + responseSender: consuming HTTPResponseSender ) async throws { - try await self._handler(request, requestConcludingAsyncReader, sendResponse) + try await self._handler(request, requestBodyAndTrailers, responseSender) } } diff --git a/Sources/HTTPServer/HTTPServerProtocol.swift b/Sources/HTTPServer/HTTPServerProtocol.swift new file mode 100644 index 0000000..6e46edd --- /dev/null +++ b/Sources/HTTPServer/HTTPServerProtocol.swift @@ -0,0 +1,79 @@ +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) + + /// The ``HTTPServerRequestHandler`` to use when handling requests. + associatedtype RequestHandler: HTTPServerRequestHandler + + /// Starts an HTTP server with the specified request handler. + /// + /// This method creates and runs an HTTP server that processes incoming requests using the provided + /// ``HTTPServerRequestHandler`` implementation. + /// + /// Implementations of this method should handle each connection concurrently using Swift's structured concurrency. + /// + /// - Parameters: + /// - handler: A ``HTTPServerRequestHandler`` implementation that processes incoming HTTP requests. The handler + /// receives each request along with a body reader and ``HTTPResponseSender``. + /// + /// ## Example + /// + /// ```swift + /// struct EchoHandler: HTTPServerRequestHandler { + /// func handle( + /// request: HTTPRequest, + /// requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader, + /// responseSender: consuming HTTPResponseSender + /// ) async throws { + /// let response = HTTPResponse(status: .ok) + /// let writer = try await responseSender.send(response) + /// // Handle request and write response... + /// } + /// } + /// + /// let server = // create an instance of a type conforming to the `ServerProtocol` + /// + /// try await server.serve(handler: EchoHandler()) + /// ``` + 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: @Sendable @escaping ( + _ request: HTTPRequest, + _ requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader, + _ responseSender: consuming 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 1c75a42..ab9b842 100644 --- a/Sources/HTTPServer/HTTPServerRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerRequestHandler.swift @@ -3,80 +3,101 @@ 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:requestConcludingAsyncReader:sendResponse:)`` method which is called by the HTTP server -/// for each incoming request. The handler is responsible for: +/// Conforming types implement the ``handle(request: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`` /// - Generating and sending an appropriate response using the response callback /// -/// This protocol supports fully bi-directional streaming HTTP request handling including the optional request -/// and response trailers +/// This protocol fully supports bi-directional streaming HTTP request handling including the optional request and response trailers. +/// +/// # Example /// -/// - Example: /// ```swift /// struct EchoHandler: HTTPServerRequestHandler { -/// func handle( -/// request: HTTPRequest, -/// requestConcludingAsyncReader: HTTPRequestConcludingAsyncReader, -/// sendResponse: @escaping (HTTPResponse) async throws -> HTTPResponseConcludingAsyncWriter -/// ) async throws { -/// // Read the entire request body -/// let (bodyData, trailers) = try await requestConcludingAsyncReader.consumeAndConclude { reader in -/// var data = [UInt8]() -/// var shouldContinue = true -/// while shouldContinue { -/// try await reader.read { span in -/// guard let span else { -/// shouldContinue = false -/// return -/// } -/// data.reserveCapacity(data.count + span.count) -/// for index in span.indices { -/// data.append(span[index]) -/// } +/// func handle( +/// request: HTTPRequest, +/// requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader, +/// responseSender: consuming HTTPResponseSender +/// ) async throws { +/// // Read the entire request body +/// let (bodyData, trailers) = try await requestConcludingAsyncReader.consumeAndConclude { reader in +/// var reader = reader +/// var data = [UInt8]() +/// var shouldContinue = true +/// while shouldContinue { +/// try await reader.read { span in +/// guard let span else { +/// shouldContinue = false +/// return +/// } +/// data.reserveCapacity(data.count + span.count) +/// for index in span.indices { +/// data.append(span[index]) /// } /// } -/// return data /// } +/// return data +/// } /// -/// // Create a response -/// var response = HTTPResponse(status: .ok) -/// response.headerFields[.contentType] = "text/plain" +/// // Create a response +/// var response = HTTPResponse(status: .ok) +/// response.headerFields[.contentType] = "text/plain" /// -/// // Send the response and write the echo data back -/// let responseWriter = try await sendResponse(response) -/// try await responseWriter.produceAndConclude { writer in -/// try await writer.write(bodyData.span) -/// return ((), nil) // No trailers -/// } +/// // Send the response and write the echo data back +/// let responseWriter = try await responseSender.send(response) +/// try await responseWriter.produceAndConclude { writer in +/// var writer = writer +/// try await writer.write(bodyData.span) +/// return ((), nil) // No trailers /// } +/// } /// } /// ``` @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) 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 + + /// 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`` + /// 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 + /// 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 `requestConcludingAsyncReader` as needed + /// 2. Read the request body data from the ``RequestConcludingAsyncReader`` as needed /// 3. Process the request and prepare a response - /// 4. Call the `sendResponse` function with an appropriate HTTP response - /// 5. Write the response body data to the returned `HTTPResponseConcludingAsyncWriter` + /// 4. Optionally call ``HTTPResponseSender/sendInformationalResponse(_:)`` as needed + /// 4. Call the ``HTTPResponseSender/sendResponse(_:)`` with an appropriate HTTP response + /// 5. Write the response body data to the returned ``HTTPResponseConcludingAsyncWriter`` /// /// - Parameters: /// - request: The HTTP request headers and metadata. - /// - requestConcludingAsyncReader: A reader for accessing the request body data and trailing headers. + /// - 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. - /// - sendResponse: A callback function that takes an HTTP response and returns a writer for the response body. - /// This function should be called exactly once to initiate the response. The returned writer allows for - /// incremental writing of response body data and concluding with optional trailer fields. + /// - responseSender: An ``HTTPResponseSender`` that takes an HTTP response and returns a writer for the + /// response body. The returned writer allows for the incremental writing of the response body, and supports trailers. /// /// - Throws: Any error encountered during request processing or response generation. func handle( + // TODO: add request context parameter request: HTTPRequest, - requestConcludingAsyncReader: consuming HTTPRequestConcludingAsyncReader, - sendResponse: consuming HTTPResponseSender + requestBodyAndTrailers: consuming ConcludingRequestReader, + responseSender: consuming HTTPResponseSender ) async throws } diff --git a/Sources/HTTPServer/HTTPServer.swift b/Sources/HTTPServer/NIOHTTPServer.swift similarity index 81% rename from Sources/HTTPServer/HTTPServer.swift rename to Sources/HTTPServer/NIOHTTPServer.swift index 53b9d68..566619a 100644 --- a/Sources/HTTPServer/HTTPServer.swift +++ b/Sources/HTTPServer/NIOHTTPServer.swift @@ -1,5 +1,5 @@ -public import HTTPTypes public import Logging +import HTTPTypes import NIOCertificateReloading import NIOCore import NIOHTTP1 @@ -62,55 +62,22 @@ import Synchronization /// } /// ``` @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -public final class Server { - /// 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. - /// The server will bind to the specified configuration and process requests asynchronously. - /// +public struct NIOHTTPServer: HTTPServerProtocol +where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader, + RequestHandler.ConcludingResponseWriter == HTTPResponseConcludingAsyncWriter { + private let logger: Logger + private let configuration: HTTPServerConfiguration + + /// Create a new ``HTTPServer`` implemented over `SwiftNIO`. /// - Parameters: /// - logger: A logger instance for recording server events and debugging information. /// - configuration: The server configuration including bind target and TLS settings. - /// - 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 - /// - A non-copyable response sender function that accepts an `HTTPResponse` and provides access to an `HTTPResponseConcludingAsyncWriter` - /// - /// ## Example - /// - /// ```swift - /// let configuration = HTTPServerConfiguration( - /// bindTarget: .hostAndPort(host: "localhost", port: 8080), - /// tlsConfiguration: .insecure() - /// ) - /// - /// try await Server.serve( - /// logger: logger, - /// configuration: configuration - /// ) { 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 static func serve( + public init( logger: Logger, configuration: HTTPServerConfiguration, - handler: @Sendable @escaping ( - HTTPRequest, - consuming HTTPRequestConcludingAsyncReader, - consuming HTTPResponseSender - ) async throws -> Void - ) async throws where RequestHandler == HTTPServerClosureRequestHandler { - try await self.serve( - logger: logger, - configuration: configuration, - handler: HTTPServerClosureRequestHandler(handler: handler) - ) + ) { + self.logger = logger + self.configuration = configuration } /// Starts an HTTP server with the specified request handler. @@ -131,8 +98,8 @@ public final class Server { /// struct EchoHandler: HTTPServerRequestHandler { /// func handle( /// request: HTTPRequest, - /// requestConcludingAsyncReader: HTTPRequestConcludingAsyncReader, - /// sendResponse: @escaping (HTTPResponse) async throws -> HTTPResponseConcludingAsyncWriter + /// requestBodyAndTrailers: HTTPRequestConcludingAsyncReader, + /// responseSender: @escaping (HTTPResponse) async throws -> HTTPResponseConcludingAsyncWriter /// ) async throws { /// let response = HTTPResponse(status: .ok) /// let writer = try await sendResponse(response) @@ -151,13 +118,9 @@ public final class Server { /// handler: EchoHandler() /// ) /// ``` - public static func serve( - logger: Logger, - configuration: HTTPServerConfiguration, - handler: RequestHandler - ) async throws { + public func serve(handler: RequestHandler) async throws { let asyncChannelConfiguration: NIOAsyncChannel.Configuration - switch configuration.backpressureStrategy.backing { + switch self.configuration.backpressureStrategy.backing { case .watermark(let low, let high): asyncChannelConfiguration = .init( backPressureStrategy: .init(lowWatermark: low, highWatermark: high), @@ -165,18 +128,17 @@ public final class Server { ) } - switch configuration.transportSecurity.backing { + switch self.configuration.transportSecurity.backing { case .plaintext: - try await Self.serveInsecureHTTP1_1( - bindTarget: configuration.bindTarget, + try await self.serveInsecureHTTP1_1( + bindTarget: self.configuration.bindTarget, handler: handler, - asyncChannelConfiguration: asyncChannelConfiguration, - logger: logger + asyncChannelConfiguration: asyncChannelConfiguration ) case .tls(let certificateChain, let privateKey): let http2Config = NIOHTTP2Handler.Configuration( - httpServerHTTP2Configuration: configuration.http2 + httpServerHTTP2Configuration: self.configuration.http2 ) let certificateChain = try certificateChain @@ -200,13 +162,12 @@ public final class Server { ) tlsConfiguration.applicationProtocols = ["h2", "http/1.1"] - try await Self.serveSecureUpgrade( + try await self.serveSecureUpgrade( bindTarget: configuration.bindTarget, tlsConfiguration: tlsConfiguration, handler: handler, asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Config, - logger: logger + http2Configuration: http2Config ) case .reloadingTLS(let certificateReloader): @@ -219,13 +180,12 @@ public final class Server { ) tlsConfiguration.applicationProtocols = ["h2", "http/1.1"] - try await Self.serveSecureUpgrade( + try await self.serveSecureUpgrade( bindTarget: configuration.bindTarget, tlsConfiguration: tlsConfiguration, handler: handler, asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Config, - logger: logger + http2Configuration: http2Config ) case .mTLS(let certificateChain, let privateKey, let trustRoots): @@ -269,13 +229,12 @@ public final class Server { ) tlsConfiguration.applicationProtocols = ["h2", "http/1.1"] - try await Self.serveSecureUpgrade( + try await self.serveSecureUpgrade( bindTarget: configuration.bindTarget, tlsConfiguration: tlsConfiguration, handler: handler, asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Config, - logger: logger + http2Configuration: http2Config ) case .reloadingMTLS(let certificateReloader, let trustRoots): @@ -303,22 +262,20 @@ public final class Server { ) tlsConfiguration.applicationProtocols = ["h2", "http/1.1"] - try await Self.serveSecureUpgrade( + try await self.serveSecureUpgrade( bindTarget: configuration.bindTarget, tlsConfiguration: tlsConfiguration, handler: handler, asyncChannelConfiguration: asyncChannelConfiguration, - http2Configuration: http2Config, - logger: logger + http2Configuration: http2Config ) } } - private static func serveInsecureHTTP1_1( + private func serveInsecureHTTP1_1( bindTarget: HTTPServerConfiguration.BindTarget, handler: RequestHandler, - asyncChannelConfiguration: NIOAsyncChannel.Configuration, - logger: Logger + asyncChannelConfiguration: NIOAsyncChannel.Configuration ) async throws { switch bindTarget.backing { case .hostAndPort(let host, let port): @@ -338,8 +295,7 @@ public final class Server { try await serverChannel.executeThenClose { inbound in for try await http1Channel in inbound { group.addTask { - try await Self.handleRequestChannel( - logger: logger, + try await self.handleRequestChannel( channel: http1Channel, handler: handler ) @@ -350,13 +306,12 @@ public final class Server { } } - private static func serveSecureUpgrade( + private func serveSecureUpgrade( bindTarget: HTTPServerConfiguration.BindTarget, tlsConfiguration: TLSConfiguration, handler: RequestHandler, asyncChannelConfiguration: NIOAsyncChannel.Configuration, - http2Configuration: NIOHTTP2Handler.Configuration, - logger: Logger + http2Configuration: NIOHTTP2Handler.Configuration ) async throws { switch bindTarget.backing { case .hostAndPort(let host, let port): @@ -407,8 +362,7 @@ public final class Server { switch try await upgradeResult.get() { case .http1_1(let http1Channel): connectionGroup.addTask { - try await Self.handleRequestChannel( - logger: logger, + try await self.handleRequestChannel( channel: http1Channel, handler: handler ) @@ -417,20 +371,19 @@ public final class Server { do { for try await http2StreamChannel in http2Multiplexer.inbound { connectionGroup.addTask { - try await Self.handleRequestChannel( - logger: logger, + try await self.handleRequestChannel( channel: http2StreamChannel, handler: handler ) } } } catch { - logger.debug("HTTP2 connection closed: \(error)") + self.logger.debug("HTTP2 connection closed: \(error)") } } } } catch { - logger.debug("Negotiating ALPN failed: \(error)") + self.logger.debug("Negotiating ALPN failed: \(error)") } } } @@ -439,8 +392,7 @@ public final class Server { } } - private static func handleRequestChannel( - logger: Logger, + private func handleRequestChannel( channel: NIOAsyncChannel, handler: RequestHandler ) async throws { @@ -454,15 +406,15 @@ public final class Server { case .head(let request): httpRequest = request case .body: - logger.debug("Unexpectedly received body on connection. Closing now") + self.logger.debug("Unexpectedly received body on connection. Closing now") outbound.finish() return case .end: - logger.debug("Unexpectedly received end on connection. Closing now") + self.logger.debug("Unexpectedly received end on connection. Closing now") outbound.finish() return case .none: - logger.trace("No more requests parts on connection") + self.logger.trace("No more requests parts on connection") return } @@ -472,16 +424,18 @@ public final class Server { do { try await handler.handle( request: httpRequest, - requestConcludingAsyncReader: HTTPRequestConcludingAsyncReader( + requestBodyAndTrailers: HTTPRequestConcludingAsyncReader( iterator: iterator, readerState: readerState ), - sendResponse: HTTPResponseSender { response in + responseSender: HTTPResponseSender { response in try await outbound.write(.head(response)) return HTTPResponseConcludingAsyncWriter( writer: outbound, writerState: writerState ) + } sendInformational: { response in + try await outbound.write(.head(response)) } ) } catch { @@ -511,7 +465,7 @@ public final class Server { try await channel.channel.closeFuture.get() } } catch { - logger.error("Error thrown while handling connection: \(error)") + self.logger.debug("Error thrown while handling connection: \(error)") // TODO: We need to send a response head here potentially throw error } diff --git a/Tests/HTTPServerTests/HTTPServerTests.swift b/Tests/HTTPServerTests/HTTPServerTests.swift index b6d436c..94ad3f5 100644 --- a/Tests/HTTPServerTests/HTTPServerTests.swift +++ b/Tests/HTTPServerTests/HTTPServerTests.swift @@ -8,45 +8,36 @@ struct HTTPServerTests { @Test @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) func testConsumingServe() async throws { - try await HTTPServer.Server - .serve( - logger: Logger(label: "Test"), - configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 0)), - handler: HTTPServerTests.executeTestBody(request:requestReader:responseSender:) - ) - } + 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 requestBodyAndTrailers.collect(upTo: 100) { _ in } + // Uncommenting this would cause a "requestReader consumed more than once" error. + //_ = try await requestReader.collect(upTo: 100) { _ in } - @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) - @Sendable - nonisolated(nonsending) static func executeTestBody( - request: HTTPRequest, - requestReader: consuming HTTPRequestConcludingAsyncReader, - responseSender: consuming HTTPResponseSender - ) async throws { - _ = try await requestReader.collect(upTo: 100) { _ in } - // Uncommenting this would cause a "requestReader consumed more than once" error. - //_ = try await requestReader.collect(upTo: 100) { _ in } + let responseConcludingWriter = try await responseSender.send(HTTPResponse(status: .ok)) + // Uncommenting this would cause a "responseSender consumed more than once" error. + //let responseConcludingWriter2 = try await responseSender.send(HTTPResponse(status: .ok)) - let responseConcludingWriter = try await responseSender.sendResponse(HTTPResponse(status: .ok)) - // Uncommenting this would cause a "responseSender consumed more than once" error. - //let responseConcludingWriter2 = try await responseSender.sendResponse(HTTPResponse(status: .ok)) + // Uncommenting this would cause a "requestReader consumed more than once" error. + //_ = try await requestReader.consumeAndConclude { reader in + // var reader = reader + // try await reader.read { elem in } + //} - // Uncommenting this would cause a "requestReader consumed more than once" error. - //_ = try await requestReader.consumeAndConclude { reader in - // var reader = reader - // try await reader.read { elem in } - //} + try await responseConcludingWriter.produceAndConclude { writer in + var writer = writer + try await writer.write([1,2].span) + return nil + } - try await responseConcludingWriter.produceAndConclude { writer in - var writer = writer - try await writer.write([1,2].span) - return nil + // Uncommenting this would cause a "responseConcludingWriter consumed more than once" error. + //try await responseConcludingWriter.writeAndConclude( + // element: [1, 2].span, + // finalElement: HTTPFields(dictionaryLiteral: (.acceptEncoding, "Encoding")) + //) } - - // Uncommenting this would cause a "responseConcludingWriter consumed more than once" error. - //try await responseConcludingWriter.writeAndConclude( - // element: [1, 2].span, - // finalElement: HTTPFields(dictionaryLiteral: (.acceptEncoding, "Encoding")) - //) } }