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
16 changes: 3 additions & 13 deletions Sources/Example/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTTPServerClosureRequestHandler<
HTTPRequestConcludingAsyncReader,
HTTPRequestConcludingAsyncReader.Underlying,
HTTPResponseConcludingAsyncWriter,
HTTPResponseConcludingAsyncWriter.Underlying
>>(
let server = NIOHTTPServer(
logger: logger,
configuration: .init(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345),
Expand All @@ -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)
Expand All @@ -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(
Expand Down
27 changes: 11 additions & 16 deletions Sources/HTTPServer/HTTPServerClosureRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ public import HTTPTypes
/// ```
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
public struct HTTPServerClosureRequestHandler<
ConcludingRequestReader: ConcludingAsyncReader<RequestReader, HTTPFields?> & ~Copyable,
ConcludingRequestReader: ConcludingAsyncReader<RequestReader, HTTPFields?> & ~Copyable & SendableMetatype,
RequestReader: AsyncReader<Span<UInt8>, any Error> & ~Copyable,
ConcludingResponseWriter: ConcludingAsyncWriter<RequestWriter, HTTPFields?> & ~Copyable,
ConcludingResponseWriter: ConcludingAsyncWriter<RequestWriter, HTTPFields?> & ~Copyable & SendableMetatype,
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>
consuming sending ConcludingRequestReader,
consuming sending HTTPResponseSender<ConcludingResponseWriter>
) async throws -> Void

/// Creates a new closure-based HTTP request handler.
Expand All @@ -47,8 +47,8 @@ public struct HTTPServerClosureRequestHandler<
handler: nonisolated(nonsending) @Sendable @escaping (
HTTPRequest,
HTTPRequestContext,
consuming sending HTTPRequestConcludingAsyncReader,
consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
consuming sending ConcludingRequestReader,
consuming sending HTTPResponseSender<ConcludingResponseWriter>
) async throws -> Void
) {
self._handler = handler
Expand All @@ -66,20 +66,15 @@ public struct HTTPServerClosureRequestHandler<
public func handle(
request: HTTPRequest,
requestContext: HTTPRequestContext,
requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader,
responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
requestBodyAndTrailers: consuming sending ConcludingRequestReader,
responseSender: consuming sending HTTPResponseSender<ConcludingResponseWriter>
) 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.
Expand Down Expand Up @@ -108,8 +103,8 @@ extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHan
handler: @Sendable @escaping (
_ request: HTTPRequest,
_ requestContext: HTTPRequestContext,
_ requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader,
_ responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
_ requestBodyAndTrailers: consuming sending RequestReader,
_ responseSender: consuming sending HTTPResponseSender<ResponseWriter>
) async throws -> Void
) async throws {
try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler))
Expand Down
27 changes: 19 additions & 8 deletions Sources/HTTPServer/HTTPServerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<UInt8>` as its
/// `ReadElement`.
associatedtype RequestReader: ConcludingAsyncReader & ~Copyable & SendableMetatype
where RequestReader.Underlying.ReadElement == Span<UInt8>,
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<UInt8>` as its
/// `WriteElement`.
associatedtype ResponseWriter: ConcludingAsyncWriter & ~Copyable & SendableMetatype
where ResponseWriter.Underlying.WriteElement == Span<UInt8>,
ResponseWriter.Underlying.WriteFailure == any Error,
ResponseWriter.FinalElement == HTTPFields?

/// Starts an HTTP server with the specified request handler.
///
Expand All @@ -18,16 +28,17 @@ 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
///
/// ```swift
/// struct EchoHandler: HTTPServerRequestHandler {
/// func handle(
/// request: HTTPRequest,
/// requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader,
/// responseSender: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
/// requestContext: HTTPRequestContext,
/// requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader,
/// responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
/// ) async throws {
/// let response = HTTPResponse(status: .ok)
/// let writer = try await responseSender.send(response)
Expand All @@ -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<RequestReader, ResponseWriter>) async throws
}
32 changes: 14 additions & 18 deletions Sources/HTTPServer/HTTPServerRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<RequestReader, ResponseWriter>: Sendable {
/// The ``ConcludingAsyncReader`` to use when reading requests. ``ConcludingAsyncReader/FinalElement``
/// must be an optional `HTTPFields`, and ``ConcludingAsyncReader/Underlying`` must use `Span<UInt8>` as its
/// `ReadElement`.
associatedtype ConcludingRequestReader: ConcludingAsyncReader<RequestReader, HTTPFields?> & ~Copyable
associatedtype RequestReader: ConcludingAsyncReader & ~Copyable & SendableMetatype
where RequestReader.Underlying.ReadElement == Span<UInt8>,
RequestReader.FinalElement == HTTPFields?

/// The underlying ``AsyncReader`` for ``ConcludingRequestReader``. Its ``AsyncReader/ReadElement`` must
/// be `Span<UInt8>`.
associatedtype RequestReader: AsyncReader<Span<UInt8>, 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<UInt8>` as its
/// `WriteElement`.
associatedtype ConcludingResponseWriter: ConcludingAsyncWriter<RequestWriter, HTTPFields?> & ~Copyable

/// The underlying ``AsyncWriter`` for ``ConcludingResponseWriter``. Its ``AsyncWriter/WriteElement`` must
/// be `Span<UInt8>`.
associatedtype RequestWriter: AsyncWriter<Span<UInt8>, any Error> & ~Copyable
associatedtype ResponseWriter: ConcludingAsyncWriter & ~Copyable & SendableMetatype
where ResponseWriter.Underlying.WriteElement == Span<UInt8>,
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.
Expand All @@ -99,7 +95,7 @@ public protocol HTTPServerRequestHandler: Sendable {
func handle(
request: HTTPRequest,
requestContext: HTTPRequestContext,
requestBodyAndTrailers: consuming sending ConcludingRequestReader,
responseSender: consuming sending HTTPResponseSender<ConcludingResponseWriter>
requestBodyAndTrailers: consuming sending RequestReader,
responseSender: consuming sending HTTPResponseSender<ResponseWriter>
) async throws
}
15 changes: 8 additions & 7 deletions Sources/HTTPServer/NIOHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestHandler: HTTPServerRequestHandler>: 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

Expand Down Expand Up @@ -118,7 +119,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader
/// handler: EchoHandler()
/// )
/// ```
public func serve(handler: RequestHandler) async throws {
public func serve(handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>) async throws {
let asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
switch self.configuration.backpressureStrategy.backing {
case .watermark(let low, let high):
Expand Down Expand Up @@ -274,7 +275,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader

private func serveInsecureHTTP1_1(
bindTarget: HTTPServerConfiguration.BindTarget,
handler: RequestHandler,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
) async throws {
switch bindTarget.backing {
Expand Down Expand Up @@ -309,7 +310,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader
private func serveSecureUpgrade(
bindTarget: HTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
handler: RequestHandler,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration
) async throws {
Expand Down Expand Up @@ -394,7 +395,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader

private func handleRequestChannel(
channel: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>,
handler: RequestHandler
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>
) async throws {
do {
try await channel
Expand Down
7 changes: 1 addition & 6 deletions Tests/HTTPServerTests/HTTPServerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTTPServerClosureRequestHandler<
HTTPRequestConcludingAsyncReader,
HTTPRequestConcludingAsyncReader.RequestBodyAsyncReader,
HTTPResponseConcludingAsyncWriter,
HTTPResponseConcludingAsyncWriter.ResponseBodyAsyncWriter
>>(
let server = NIOHTTPServer(
logger: Logger(label: "Test"),
configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 0))
)
Expand Down
Loading