Skip to content
Merged
33 changes: 11 additions & 22 deletions Sources/Example/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTTPServerClosureRequestHandler<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter>>(
logger: logger,
configuration: .init(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345),
Expand All @@ -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<HTTPResponseConcludingAsyncWriter>
) 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)
}
}
}

Expand All @@ -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<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter> {
/// 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<
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,18 @@ 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
)
} 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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Example/Middlewares/RouteHandlerMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
38 changes: 27 additions & 11 deletions Sources/HTTPServer/HTTPResponseSender.swift
Original file line number Diff line number Diff line change
@@ -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<HTTPResponseConcludingAsyncWriter>)->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<ResponseWriter: ConcludingAsyncWriter & ~Copyable>: ~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)
}
}

Expand Down
14 changes: 7 additions & 7 deletions Sources/HTTPServer/HTTPServerClosureRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConcludingRequestReader: ~Copyable, ConcludingResponseWriter: ~Copyable>: HTTPServerRequestHandler {
/// The underlying closure that handles HTTP requests
private let _handler:
nonisolated(nonsending) @Sendable (
Expand All @@ -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,
Expand All @@ -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<HTTPResponseConcludingAsyncWriter>
requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader,
responseSender: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
) async throws {
try await self._handler(request, requestConcludingAsyncReader, sendResponse)
try await self._handler(request, requestBodyAndTrailers, responseSender)
}
}
79 changes: 79 additions & 0 deletions Sources/HTTPServer/HTTPServerProtocol.swift
Original file line number Diff line number Diff line change
@@ -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<HTTPResponseConcludingAsyncWriter>
/// ) 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<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter> {
/// 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<HTTPResponseConcludingAsyncWriter>
) async throws -> Void
) async throws {
try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler))
}
}
Loading
Loading