Skip to content

Commit 8f1c17e

Browse files
authored
Add a ServerProtocol (#25)
1 parent c328b4f commit 8f1c17e

File tree

9 files changed

+267
-213
lines changed

9 files changed

+267
-213
lines changed

Sources/Example/Example.swift

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct Example {
2222

2323
// Using the new extension method that doesn't require type hints
2424
let privateKey = P256.Signing.PrivateKey()
25-
try await Server.serve(
25+
let server = NIOHTTPServer<HTTPServerClosureRequestHandler<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter>>(
2626
logger: logger,
2727
configuration: .init(
2828
bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345),
@@ -43,18 +43,12 @@ struct Example {
4343
],
4444
privateKey: Certificate.PrivateKey(privateKey)
4545
)
46-
), handler: handler(request:requestConcludingAsyncReader:responseSender:))
47-
}
48-
49-
// This is a workaround for a current bug with the compiler.
50-
@Sendable
51-
nonisolated(nonsending) private static func handler(
52-
request: HTTPRequest,
53-
requestConcludingAsyncReader: consuming HTTPRequestConcludingAsyncReader,
54-
responseSender: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
55-
) async throws {
56-
let writer = try await responseSender.sendResponse(HTTPResponse(status: .ok))
57-
try await writer.writeAndConclude(element: "Well, hello!".utf8.span, finalElement: nil)
46+
)
47+
)
48+
try await server.serve { request, requestBodyAndTrailers, responseSender in
49+
let writer = try await responseSender.send(HTTPResponse(status: .ok))
50+
try await writer.writeAndConclude(element: "Well, hello!".utf8.span, finalElement: nil)
51+
}
5852
}
5953
}
6054

@@ -63,12 +57,10 @@ struct Example {
6357
// This has to be commented out because of the compiler bug above. Workaround doesn't apply here.
6458

6559
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
66-
extension Server {
60+
extension NIOHTTPServer where RequestHandler == HTTPServerClosureRequestHandler<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter> {
6761
/// Serve HTTP requests using a middleware chain built with the provided builder
6862
/// This method handles the type inference for HTTP middleware components
69-
static func serve(
70-
logger: Logger,
71-
configuration: HTTPServerConfiguration,
63+
func serve(
7264
@MiddlewareChainBuilder
7365
withMiddleware middlewareBuilder: () -> some Middleware<
7466
RequestResponseMiddlewareBox<
@@ -77,13 +69,10 @@ extension Server {
7769
>,
7870
Never
7971
> & Sendable
80-
) async throws where RequestHandler == HTTPServerClosureRequestHandler {
72+
) async throws {
8173
let chain = middlewareBuilder()
8274

83-
try await serve(
84-
logger: logger,
85-
configuration: configuration
86-
) { request, reader, responseSender in
75+
try await self.serve { request, reader, responseSender in
8776
try await chain.intercept(input: RequestResponseMiddlewareBox(
8877
request: request,
8978
requestReader: reader,

Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,18 @@ where
5050
requestReader: wrappedReader,
5151
responseSender: HTTPResponseSender { [logger] response in
5252
if let sender = maybeSender.take() {
53-
let writer = try await sender.sendResponse(response)
53+
logger.info("Sending response \(response)")
54+
let writer = try await sender.send(response)
5455
return HTTPResponseLoggingConcludingAsyncWriter(
5556
base: writer,
5657
logger: logger
5758
)
5859
} else {
5960
fatalError("Called closure more than once")
6061
}
62+
} sendInformational: { response in
63+
self.logger.info("Sending informational response \(response)")
64+
try await maybeSender?.sendInformational(response)
6165
}
6266
)
6367
try await next(requestResponseBox)

Sources/Example/Middlewares/RouteHandlerMiddleware.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ where
2222
) async throws {
2323
try await input.withContents { request, requestReader, responseSender in
2424
var maybeReader = Optional(requestReader)
25-
try await responseSender.sendResponse(HTTPResponse(status: .accepted))
25+
try await responseSender.send(HTTPResponse(status: .accepted))
2626
.produceAndConclude { responseBodyAsyncWriter in
2727
var responseBodyAsyncWriter = responseBodyAsyncWriter
2828
if let reader = maybeReader.take() {

Sources/HTTPServer/HTTPResponseSender.swift

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,41 @@
11
public import HTTPTypes
22

3-
/// This type ensures that a single `HTTPResponse` is sent back to the client when handling a request with
4-
/// ``Server/serve(logger:configuration:handler:)-(_,_,RequestHandler)`` or ``Server/serve(logger:configuration:handler:)-(_,_,(HTTPRequest,HTTPRequestConcludingAsyncReader,HTTPResponseSender<HTTPResponseConcludingAsyncWriter>)->Void)``.
3+
/// This type ensures that a single non-informational (1xx) `HTTPResponse` is sent back to the client when handling a request.
54
///
6-
/// The user will get a ``HTTPResponseSender`` as part of the handler, and they will only be allowed to call ``sendResponse(_:)``
7-
/// once before the sender is consumed and cannot be referenced again. This forces structure in the response flow, requiring users to
8-
/// send a single response before they can stream a response body and trailers using the returned `ResponseWriter`.
5+
/// The user will get a ``HTTPResponseSender`` as part of
6+
/// ``HTTPServerRequestHandler/handle(request:requestBodyAndTrailers:responseSender:)``, and they
7+
/// will only be allowed to call ``send(_:)`` once before the sender is consumed and cannot be referenced again.
8+
/// ``sendInformational(_:)`` may be called zero or more times.
9+
///
10+
/// This forces structure in the response flow, requiring users to send a single response before they can stream a response body and
11+
/// trailers using the returned `ResponseWriter`.
912
public struct HTTPResponseSender<ResponseWriter: ConcludingAsyncWriter & ~Copyable>: ~Copyable {
10-
private let _sendResponse: (HTTPResponse) async throws -> ResponseWriter
13+
private let _sendInformational: (HTTPResponse) async throws -> Void
14+
private let _send: (HTTPResponse) async throws -> ResponseWriter
1115

1216
public init(
13-
_ sendResponse: @escaping (HTTPResponse) async throws -> ResponseWriter
17+
send: @escaping (HTTPResponse) async throws -> ResponseWriter,
18+
sendInformational: @escaping (HTTPResponse) async throws -> Void
1419
) {
15-
self._sendResponse = sendResponse
20+
self._send = send
21+
self._sendInformational = sendInformational
1622
}
1723

1824
/// Send the given `HTTPResponse` and get back a `ResponseWriter` to which to write a response body and trailers.
19-
/// - Parameter response: The `HTTPResponse` to send back to the client.
25+
/// - Parameter response: The final `HTTPResponse` to send back to the client.
2026
/// - Returns: The `ResponseWriter` to which to write a response body and trailers.
21-
consuming public func sendResponse(_ response: HTTPResponse) async throws -> ResponseWriter {
22-
try await self._sendResponse(response)
27+
/// - Important: Note this method is consuming: after you send this response, you won't be able to send any more responses.
28+
/// If you need to send an informational (1xx) response, use ``sendInformational(_:)`` instead.
29+
consuming public func send(_ response: HTTPResponse) async throws -> ResponseWriter {
30+
precondition(response.status.kind != .informational)
31+
return try await self._send(response)
32+
}
33+
34+
/// Send the given informational (1xx) response.
35+
/// - Parameter response: An informational `HTTPResponse` to send back to the client.
36+
public func sendInformational(_ response: HTTPResponse) async throws {
37+
precondition(response.status.kind == .informational)
38+
return try await _sendInformational(response)
2339
}
2440
}
2541

Sources/HTTPServer/HTTPServerClosureRequestHandler.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public import HTTPTypes
2424
/// }
2525
/// ```
2626
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
27-
public struct HTTPServerClosureRequestHandler: HTTPServerRequestHandler {
27+
public struct HTTPServerClosureRequestHandler<ConcludingRequestReader: ~Copyable, ConcludingResponseWriter: ~Copyable>: HTTPServerRequestHandler {
2828
/// The underlying closure that handles HTTP requests
2929
private let _handler:
3030
nonisolated(nonsending) @Sendable (
@@ -36,7 +36,7 @@ public struct HTTPServerClosureRequestHandler: HTTPServerRequestHandler {
3636
/// Creates a new closure-based HTTP request handler.
3737
///
3838
/// - Parameter handler: A closure that will be called to handle each incoming HTTP request.
39-
/// The closure takes the same parameters as the ``HTTPServerRequestHandler/handle(request:requestConcludingAsyncReader:sendResponse:)`` method.
39+
/// The closure takes the same parameters as the ``HTTPServerRequestHandler/handle(request:requestBodyAndTrailers:responseSender:)`` method.
4040
public init(
4141
handler: nonisolated(nonsending) @Sendable @escaping (
4242
HTTPRequest,
@@ -53,13 +53,13 @@ public struct HTTPServerClosureRequestHandler: HTTPServerRequestHandler {
5353
///
5454
/// - Parameters:
5555
/// - request: The HTTP request headers and metadata.
56-
/// - requestConcludingAsyncReader: A reader for accessing the request body data and trailing headers.
57-
/// - sendResponse: A callback function to send the HTTP response.
56+
/// - requestBodyAndTrailers: A reader for accessing the request body data and trailing headers.
57+
/// - responseSender: An ``HTTPResponseSender`` to send the HTTP response.
5858
public func handle(
5959
request: HTTPRequest,
60-
requestConcludingAsyncReader: consuming HTTPRequestConcludingAsyncReader,
61-
sendResponse: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
60+
requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader,
61+
responseSender: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
6262
) async throws {
63-
try await self._handler(request, requestConcludingAsyncReader, sendResponse)
63+
try await self._handler(request, requestBodyAndTrailers, responseSender)
6464
}
6565
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
public import HTTPTypes
2+
3+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
4+
/// A generic HTTP server protocol that can handle incoming HTTP requests.
5+
public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable {
6+
// TODO: write down in the proposal why we can't make the serve method generic over the handler (closure-based APIs can't
7+
// be implemented)
8+
9+
/// The ``HTTPServerRequestHandler`` to use when handling requests.
10+
associatedtype RequestHandler: HTTPServerRequestHandler
11+
12+
/// Starts an HTTP server with the specified request handler.
13+
///
14+
/// This method creates and runs an HTTP server that processes incoming requests using the provided
15+
/// ``HTTPServerRequestHandler`` implementation.
16+
///
17+
/// Implementations of this method should handle each connection concurrently using Swift's structured concurrency.
18+
///
19+
/// - Parameters:
20+
/// - handler: A ``HTTPServerRequestHandler`` implementation that processes incoming HTTP requests. The handler
21+
/// receives each request along with a body reader and ``HTTPResponseSender``.
22+
///
23+
/// ## Example
24+
///
25+
/// ```swift
26+
/// struct EchoHandler: HTTPServerRequestHandler {
27+
/// func handle(
28+
/// request: HTTPRequest,
29+
/// requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader,
30+
/// responseSender: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
31+
/// ) async throws {
32+
/// let response = HTTPResponse(status: .ok)
33+
/// let writer = try await responseSender.send(response)
34+
/// // Handle request and write response...
35+
/// }
36+
/// }
37+
///
38+
/// let server = // create an instance of a type conforming to the `ServerProtocol`
39+
///
40+
/// try await server.serve(handler: EchoHandler())
41+
/// ```
42+
func serve(handler: RequestHandler) async throws
43+
}
44+
45+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
46+
extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHandler<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter> {
47+
/// Starts an HTTP server with a closure-based request handler.
48+
///
49+
/// This method provides a convenient way to start an HTTP server using a closure to handle incoming requests.
50+
///
51+
/// - Parameters:
52+
/// - handler: An async closure that processes HTTP requests. The closure receives:
53+
/// - `HTTPRequest`: The incoming HTTP request with headers and metadata
54+
/// - ``HTTPRequestConcludingAsyncReader``: An async reader for consuming the request body and trailers
55+
/// - ``HTTPResponseSender``: A non-copyable wrapper for a function that accepts an `HTTPResponse` and provides access to an ``HTTPResponseConcludingAsyncWriter``
56+
///
57+
/// ## Example
58+
///
59+
/// ```swift
60+
/// try await server.serve { request, bodyReader, sendResponse in
61+
/// // Process the request
62+
/// let response = HTTPResponse(status: .ok)
63+
/// let writer = try await sendResponse(response)
64+
/// try await writer.produceAndConclude { writer in
65+
/// try await writer.write("Hello, World!".utf8)
66+
/// return ((), nil)
67+
/// }
68+
/// }
69+
/// ```
70+
public func serve(
71+
handler: @Sendable @escaping (
72+
_ request: HTTPRequest,
73+
_ requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader,
74+
_ responseSender: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
75+
) async throws -> Void
76+
) async throws {
77+
try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler))
78+
}
79+
}

0 commit comments

Comments
 (0)