Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions Sources/Example/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTTPServerClosureRequestHandler<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter>>(
let server = NIOHTTPServer<HTTPServerClosureRequestHandler<
HTTPRequestConcludingAsyncReader,
HTTPRequestConcludingAsyncReader.Underlying,
HTTPResponseConcludingAsyncWriter,
HTTPResponseConcludingAsyncWriter.Underlying
>>(
logger: logger,
configuration: .init(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345),
Expand All @@ -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)
}
Expand All @@ -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<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter> {
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
>,
Expand All @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand All @@ -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() {
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 @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Sources/HTTPServer/HTTPRequestContext.swift
Original file line number Diff line number Diff line change
@@ -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 {}
2 changes: 1 addition & 1 deletion Sources/HTTPServer/HTTPResponseSender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
60 changes: 56 additions & 4 deletions Sources/HTTPServer/HTTPServerClosureRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,33 @@ 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 ...
/// }
///
/// // 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)
/// }
/// }
/// ```
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
public struct HTTPServerClosureRequestHandler<ConcludingRequestReader: ~Copyable, ConcludingResponseWriter: ~Copyable>: HTTPServerRequestHandler {
public struct HTTPServerClosureRequestHandler<
ConcludingRequestReader: ConcludingAsyncReader<RequestReader, HTTPFields?> & ~Copyable,
RequestReader: AsyncReader<Span<UInt8>, any Error> & ~Copyable,
ConcludingResponseWriter: ConcludingAsyncWriter<RequestWriter, HTTPFields?> & ~Copyable,
RequestWriter: AsyncWriter<Span<UInt8>, 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<HTTPResponseConcludingAsyncWriter>
) async throws -> Void
Expand All @@ -40,6 +46,7 @@ public struct HTTPServerClosureRequestHandler<ConcludingRequestReader: ~Copyable
public init(
handler: nonisolated(nonsending) @Sendable @escaping (
HTTPRequest,
HTTPRequestContext,
consuming sending HTTPRequestConcludingAsyncReader,
consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
) async throws -> Void
Expand All @@ -53,13 +60,58 @@ public struct HTTPServerClosureRequestHandler<ConcludingRequestReader: ~Copyable
///
/// - Parameters:
/// - request: The HTTP request headers and metadata.
/// - requestContext: A ``HTTPRequestContext``.
/// - 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,
requestContext: HTTPRequestContext,
requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader,
responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
) 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<HTTPResponseConcludingAsyncWriter>
) async throws -> Void
) async throws {
try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler))
}
}
40 changes: 2 additions & 38 deletions Sources/HTTPServer/HTTPServerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<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: nonisolated(nonsending) @Sendable @escaping (
_ request: HTTPRequest,
_ requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader,
_ responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
) async throws -> Void
) async throws {
try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler))
}
}
16 changes: 9 additions & 7 deletions Sources/HTTPServer/HTTPServerRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ public import HTTPTypes
///
/// ```swift
/// struct EchoHandler: HTTPServerRequestHandler {
/// func handle(
/// func handle(
/// request: HTTPRequest,
/// requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader,
/// responseSender: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
/// ) async throws {
/// requestContext: HTTPRequestContext,
/// requestConcludingAsyncReader: consuming sending HTTPRequestConcludingAsyncReader,
/// responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
/// ) async throws {
/// // Read the entire request body
/// let (bodyData, trailers) = try await requestConcludingAsyncReader.consumeAndConclude { reader in
/// var reader = reader
Expand Down Expand Up @@ -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.
Expand All @@ -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<ConcludingResponseWriter>
) async throws
Expand Down
1 change: 1 addition & 0 deletions Sources/HTTPServer/NIOHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader
do {
try await handler.handle(
request: httpRequest,
requestContext: HTTPRequestContext(),
requestBodyAndTrailers: HTTPRequestConcludingAsyncReader(
iterator: iterator,
readerState: readerState
Expand Down
11 changes: 10 additions & 1 deletion Sources/HTTPServer/RequestResponseMiddlewareBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResponseWriter>

Expand All @@ -18,10 +19,12 @@ public struct RequestResponseMiddlewareBox<
/// - responseSender: The ``HTTPResponseSender``.
public init(
request: HTTPRequest,
requestContext: HTTPRequestContext,
requestReader: consuming RequestReader,
responseSender: consuming HTTPResponseSender<ResponseWriter>
) {
self.request = request
self.requestContext = requestContext
self.requestReader = requestReader
self.responseSender = responseSender
}
Expand All @@ -32,11 +35,17 @@ public struct RequestResponseMiddlewareBox<
public consuming func withContents<T>(
_ handler: nonisolated(nonsending) (
HTTPRequest,
HTTPRequestContext,
consuming RequestReader,
consuming HTTPResponseSender<ResponseWriter>
) 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
)
}
}

Expand Down
9 changes: 7 additions & 2 deletions Tests/HTTPServerTests/HTTPServerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTTPServerClosureRequestHandler<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter>>(
let server = NIOHTTPServer<HTTPServerClosureRequestHandler<
HTTPRequestConcludingAsyncReader,
HTTPRequestConcludingAsyncReader.RequestBodyAsyncReader,
HTTPResponseConcludingAsyncWriter,
HTTPResponseConcludingAsyncWriter.ResponseBodyAsyncWriter
>>(
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 }
Expand Down
Loading