From 30813b5137eb562d0f498a965f3dcd419fef43a1 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 18 Nov 2025 11:37:17 +0000 Subject: [PATCH 1/6] Add a RequestContext # Conflicts: # Sources/HTTPServer/HTTPServerClosureRequestHandler.swift # Sources/HTTPServer/HTTPServerProtocol.swift # Sources/HTTPServer/HTTPServerRequestHandler.swift --- Sources/Example/Example.swift | 27 ++++++++++++++----- .../HTTPRequestLoggingMiddleware.swift | 7 +++-- .../Middlewares/RouteHandlerMiddleware.swift | 5 ++-- Sources/HTTPServer/HTTPRequestContext.swift | 8 ++++++ .../HTTPServerClosureRequestHandler.swift | 13 +++++++-- Sources/HTTPServer/HTTPServerProtocol.swift | 10 ++++++- .../HTTPServer/HTTPServerRequestHandler.swift | 4 ++- Sources/HTTPServer/NIOHTTPServer.swift | 4 ++- .../RequestResponseMiddlewareBox.swift | 12 ++++++++- Tests/HTTPServerTests/HTTPServerTests.swift | 10 +++++-- 10 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 Sources/HTTPServer/HTTPRequestContext.swift diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index de8bb77..d2f7a32 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -22,7 +22,13 @@ struct Example { // Using the new extension method that doesn't require type hints let privateKey = P256.Signing.PrivateKey() - let server = NIOHTTPServer>( + let server = NIOHTTPServer>( logger: logger, configuration: .init( bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345), @@ -45,7 +51,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) } @@ -54,16 +60,22 @@ 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 { +extension NIOHTTPServer +where RequestHandler == HTTPServerClosureRequestHandler< + HTTPServerRequestContext, + 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< + HTTPServerRequestContext, HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter >, @@ -72,9 +84,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 } diff --git a/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift b/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift index 9617d92..a44bb3d 100644 --- a/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift +++ b/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift @@ -5,6 +5,7 @@ import Middleware @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) struct HTTPRequestLoggingMiddleware< + RequestContext: HTTPRequestContext, RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable, ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable >: Middleware @@ -14,8 +15,9 @@ where ResponseConcludingAsyncWriter.Underlying.WriteElement == Span, ResponseConcludingAsyncWriter.FinalElement == HTTPFields? { - typealias Input = RequestResponseMiddlewareBox + typealias Input = RequestResponseMiddlewareBox typealias NextInput = RequestResponseMiddlewareBox< + RequestContext, HTTPRequestLoggingConcludingAsyncReader, HTTPResponseLoggingConcludingAsyncWriter > @@ -34,7 +36,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)") @@ -47,6 +49,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() { diff --git a/Sources/Example/Middlewares/RouteHandlerMiddleware.swift b/Sources/Example/Middlewares/RouteHandlerMiddleware.swift index d4b6494..07f9be0 100644 --- a/Sources/Example/Middlewares/RouteHandlerMiddleware.swift +++ b/Sources/Example/Middlewares/RouteHandlerMiddleware.swift @@ -4,6 +4,7 @@ import Middleware @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) struct RouteHandlerMiddleware< + RequestContext: HTTPRequestContext, RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable, ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable, >: Middleware, Sendable @@ -13,14 +14,14 @@ where ResponseConcludingAsyncWriter.Underlying: AsyncWriter, any Error>, ResponseConcludingAsyncWriter.FinalElement == HTTPFields? { - typealias Input = RequestResponseMiddlewareBox + typealias Input = RequestResponseMiddlewareBox typealias NextInput = Never func intercept( 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 diff --git a/Sources/HTTPServer/HTTPRequestContext.swift b/Sources/HTTPServer/HTTPRequestContext.swift new file mode 100644 index 0000000..f87027d --- /dev/null +++ b/Sources/HTTPServer/HTTPRequestContext.swift @@ -0,0 +1,8 @@ +public protocol HTTPRequestContext: Sendable { + +} + + +public struct HTTPServerRequestContext: HTTPRequestContext { + public init() {} +} diff --git a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift index 461096f..f5c5b50 100644 --- a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift @@ -24,11 +24,18 @@ 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< + RequestContext: HTTPRequestContext, + ConcludingRequestReader: ConcludingAsyncReader & ~Copyable, + RequestReader: AsyncReader, any Error> & ~Copyable, + ConcludingResponseWriter: ConcludingAsyncWriter & ~Copyable, + RequestWriter: AsyncWriter, any Error> & ~Copyable +>: HTTPServerRequestHandler { /// The underlying closure that handles HTTP requests private let _handler: nonisolated(nonsending) @Sendable ( HTTPRequest, + RequestContext, consuming sending HTTPRequestConcludingAsyncReader, consuming sending HTTPResponseSender ) async throws -> Void @@ -40,6 +47,7 @@ public struct HTTPServerClosureRequestHandler ) async throws -> Void @@ -57,9 +65,10 @@ public struct HTTPServerClosureRequestHandler ) async throws { - try await self._handler(request, requestBodyAndTrailers, responseSender) + try await self._handler(request, requestContext, requestBodyAndTrailers, responseSender) } } diff --git a/Sources/HTTPServer/HTTPServerProtocol.swift b/Sources/HTTPServer/HTTPServerProtocol.swift index d93f1d5..5f221d7 100644 --- a/Sources/HTTPServer/HTTPServerProtocol.swift +++ b/Sources/HTTPServer/HTTPServerProtocol.swift @@ -43,7 +43,14 @@ public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable { } @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHandler { +extension HTTPServerProtocol +where RequestHandler == HTTPServerClosureRequestHandler< + HTTPServerRequestContext, + 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. @@ -70,6 +77,7 @@ extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHan public func serve( handler: nonisolated(nonsending) @Sendable @escaping ( _ request: HTTPRequest, + _ requestContext: HTTPServerRequestContext, _ requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader, _ responseSender: consuming sending HTTPResponseSender ) async throws -> Void diff --git a/Sources/HTTPServer/HTTPServerRequestHandler.swift b/Sources/HTTPServer/HTTPServerRequestHandler.swift index b209cbf..a5a61d3 100644 --- a/Sources/HTTPServer/HTTPServerRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerRequestHandler.swift @@ -75,6 +75,8 @@ public protocol HTTPServerRequestHandler: Sendable { /// be `Span`. associatedtype RequestWriter: AsyncWriter, any Error> & ~Copyable + associatedtype RequestContext: HTTPRequestContext + /// Handles an incoming HTTP request and generates a response. /// /// This method is called by the HTTP server for each incoming client request. Implementations should: @@ -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: RequestContext, requestBodyAndTrailers: consuming sending ConcludingRequestReader, responseSender: consuming sending HTTPResponseSender ) async throws diff --git a/Sources/HTTPServer/NIOHTTPServer.swift b/Sources/HTTPServer/NIOHTTPServer.swift index 566619a..5f3e676 100644 --- a/Sources/HTTPServer/NIOHTTPServer.swift +++ b/Sources/HTTPServer/NIOHTTPServer.swift @@ -64,7 +64,8 @@ import Synchronization @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) public struct NIOHTTPServer: HTTPServerProtocol where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader, - RequestHandler.ConcludingResponseWriter == HTTPResponseConcludingAsyncWriter { + RequestHandler.ConcludingResponseWriter == HTTPResponseConcludingAsyncWriter, + RequestHandler.RequestContext == HTTPServerRequestContext { private let logger: Logger private let configuration: HTTPServerConfiguration @@ -424,6 +425,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader do { try await handler.handle( request: httpRequest, + requestContext: HTTPServerRequestContext(), requestBodyAndTrailers: HTTPRequestConcludingAsyncReader( iterator: iterator, readerState: readerState diff --git a/Sources/HTTPServer/RequestResponseMiddlewareBox.swift b/Sources/HTTPServer/RequestResponseMiddlewareBox.swift index 6f634af..8bf3a4e 100644 --- a/Sources/HTTPServer/RequestResponseMiddlewareBox.swift +++ b/Sources/HTTPServer/RequestResponseMiddlewareBox.swift @@ -4,10 +4,12 @@ public import HTTPTypes /// It is necessary to box them together so that they can be used with `Middlewares`, as this will be the `Middleware.Input`. @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) public struct RequestResponseMiddlewareBox< + RequestContext: HTTPRequestContext, RequestReader: ConcludingAsyncReader & ~Copyable, ResponseWriter: ConcludingAsyncWriter & ~Copyable >: ~Copyable { private let request: HTTPRequest + private let requestContext: RequestContext private let requestReader: RequestReader private let responseSender: HTTPResponseSender @@ -18,10 +20,12 @@ public struct RequestResponseMiddlewareBox< /// - responseSender: The ``HTTPResponseSender``. public init( request: HTTPRequest, + requestContext: RequestContext, requestReader: consuming RequestReader, responseSender: consuming HTTPResponseSender ) { self.request = request + self.requestContext = requestContext self.requestReader = requestReader self.responseSender = responseSender } @@ -32,11 +36,17 @@ public struct RequestResponseMiddlewareBox< public consuming func withContents( _ handler: nonisolated(nonsending) ( HTTPRequest, + RequestContext, consuming RequestReader, consuming HTTPResponseSender ) 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 + ) } } diff --git a/Tests/HTTPServerTests/HTTPServerTests.swift b/Tests/HTTPServerTests/HTTPServerTests.swift index a1dde72..64f78d5 100644 --- a/Tests/HTTPServerTests/HTTPServerTests.swift +++ b/Tests/HTTPServerTests/HTTPServerTests.swift @@ -8,11 +8,17 @@ 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>( + 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 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 } From c9adfcc498bcd963eb54319a7a5728de118bb918 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 19 Nov 2025 16:27:25 +0000 Subject: [PATCH 2/6] Add docs --- Sources/HTTPServer/HTTPRequestContext.swift | 10 +++++++++- .../HTTPServer/HTTPServerClosureRequestHandler.swift | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/HTTPServer/HTTPRequestContext.swift b/Sources/HTTPServer/HTTPRequestContext.swift index f87027d..c544bec 100644 --- a/Sources/HTTPServer/HTTPRequestContext.swift +++ b/Sources/HTTPServer/HTTPRequestContext.swift @@ -1,8 +1,16 @@ +/// A protocol that defines the context for an HTTP request. +/// +/// Conforming types represent contextual information that can be associated with +/// an HTTP request throughout its lifecycle. public protocol HTTPRequestContext: Sendable { } - +/// The default implementation of an ``HTTPRequestContext`` for HTTP server requests. +/// +/// This struct provides a concrete type for representing the context of HTTP requests +/// handled by a server. public struct HTTPServerRequestContext: HTTPRequestContext { + /// Creates a new HTTP server request context. public init() {} } diff --git a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift index f5c5b50..5cfe44c 100644 --- a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift @@ -61,6 +61,7 @@ public struct HTTPServerClosureRequestHandler< /// /// - Parameters: /// - request: The HTTP request headers and metadata. + /// - requestContext: The request's context. /// - requestBodyAndTrailers: A reader for accessing the request body data and trailing headers. /// - responseSender: An ``HTTPResponseSender`` to send the HTTP response. public func handle( From aa2526268489a824e5396e36e7feecab920d4b20 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 20 Nov 2025 16:41:18 +0000 Subject: [PATCH 3/6] Make RequestContext a concrete type instead of protocol --- Sources/Example/Example.swift | 3 -- .../HTTPRequestLoggingMiddleware.swift | 4 +- .../Middlewares/RouteHandlerMiddleware.swift | 3 +- Sources/HTTPServer/HTTPRequestContext.swift | 45 ++++++++++++++----- .../HTTPServerClosureRequestHandler.swift | 3 +- Sources/HTTPServer/HTTPServerProtocol.swift | 1 - .../HTTPServer/HTTPServerRequestHandler.swift | 3 +- Sources/HTTPServer/NIOHTTPServer.swift | 5 +-- .../RequestResponseMiddlewareBox.swift | 7 ++- 9 files changed, 42 insertions(+), 32 deletions(-) diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index d2f7a32..e3549dc 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -23,7 +23,6 @@ struct Example { // Using the new extension method that doesn't require type hints let privateKey = P256.Signing.PrivateKey() let server = NIOHTTPServer some Middleware< RequestResponseMiddlewareBox< - HTTPServerRequestContext, HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter >, diff --git a/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift b/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift index a44bb3d..d90ae4b 100644 --- a/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift +++ b/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift @@ -5,7 +5,6 @@ import Middleware @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) struct HTTPRequestLoggingMiddleware< - RequestContext: HTTPRequestContext, RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable, ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable >: Middleware @@ -15,9 +14,8 @@ where ResponseConcludingAsyncWriter.Underlying.WriteElement == Span, ResponseConcludingAsyncWriter.FinalElement == HTTPFields? { - typealias Input = RequestResponseMiddlewareBox + typealias Input = RequestResponseMiddlewareBox typealias NextInput = RequestResponseMiddlewareBox< - RequestContext, HTTPRequestLoggingConcludingAsyncReader, HTTPResponseLoggingConcludingAsyncWriter > diff --git a/Sources/Example/Middlewares/RouteHandlerMiddleware.swift b/Sources/Example/Middlewares/RouteHandlerMiddleware.swift index 07f9be0..a6a62bb 100644 --- a/Sources/Example/Middlewares/RouteHandlerMiddleware.swift +++ b/Sources/Example/Middlewares/RouteHandlerMiddleware.swift @@ -4,7 +4,6 @@ import Middleware @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) struct RouteHandlerMiddleware< - RequestContext: HTTPRequestContext, RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable, ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable, >: Middleware, Sendable @@ -14,7 +13,7 @@ where ResponseConcludingAsyncWriter.Underlying: AsyncWriter, any Error>, ResponseConcludingAsyncWriter.FinalElement == HTTPFields? { - typealias Input = RequestResponseMiddlewareBox + typealias Input = RequestResponseMiddlewareBox typealias NextInput = Never func intercept( diff --git a/Sources/HTTPServer/HTTPRequestContext.swift b/Sources/HTTPServer/HTTPRequestContext.swift index c544bec..5201ede 100644 --- a/Sources/HTTPServer/HTTPRequestContext.swift +++ b/Sources/HTTPServer/HTTPRequestContext.swift @@ -1,16 +1,37 @@ -/// A protocol that defines the context for an HTTP request. +/// A context object that carries additional information about an HTTP request. /// -/// Conforming types represent contextual information that can be associated with -/// an HTTP request throughout its lifecycle. -public protocol HTTPRequestContext: Sendable { +/// `HTTPRequestContext` provides a way to pass metadata and implementation-specific +/// information through the HTTP request pipeline. This is particularly useful when +/// you need to attach custom data that should be available throughout the request's +/// lifecycle. +/// +/// ## Implementation-Specific Data +/// +/// Different server implementations can store their own specific data by conforming to the +/// ``ImplementationSpecific`` protocol: +/// +/// ```swift +/// struct MyCustomContext: HTTPRequestContext.ImplementationSpecific { +/// let requestID: UUID +/// let startTime: Date +/// } +/// +/// var context = HTTPRequestContext() +/// context.implementationSpecific = MyCustomContext( +/// requestID: UUID(), +/// startTime: Date() +/// ) +/// ``` +public struct HTTPRequestContext: Sendable { -} + /// Conform your custom types to this protocol to store implementation-specific + /// data within an ``HTTPRequestContext``. + public protocol ImplementationSpecific: Sendable {} -/// The default implementation of an ``HTTPRequestContext`` for HTTP server requests. -/// -/// This struct provides a concrete type for representing the context of HTTP requests -/// handled by a server. -public struct HTTPServerRequestContext: HTTPRequestContext { - /// Creates a new HTTP server request context. - public init() {} + /// Optional implementation-specific data associated with this request context. + /// + /// Use this property to store custom data that is specific to your HTTP + /// implementation. The stored value can be any type that conforms to + /// ``ImplementationSpecific``. + public var implementationSpecific: (any ImplementationSpecific)? } diff --git a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift index 5cfe44c..5c78a26 100644 --- a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift @@ -25,7 +25,6 @@ public import HTTPTypes /// ``` @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) public struct HTTPServerClosureRequestHandler< - RequestContext: HTTPRequestContext, ConcludingRequestReader: ConcludingAsyncReader & ~Copyable, RequestReader: AsyncReader, any Error> & ~Copyable, ConcludingResponseWriter: ConcludingAsyncWriter & ~Copyable, @@ -61,7 +60,7 @@ public struct HTTPServerClosureRequestHandler< /// /// - Parameters: /// - request: The HTTP request headers and metadata. - /// - requestContext: The request's context. + /// - 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( diff --git a/Sources/HTTPServer/HTTPServerProtocol.swift b/Sources/HTTPServer/HTTPServerProtocol.swift index 5f221d7..eab09eb 100644 --- a/Sources/HTTPServer/HTTPServerProtocol.swift +++ b/Sources/HTTPServer/HTTPServerProtocol.swift @@ -45,7 +45,6 @@ public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable { @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHandler< - HTTPServerRequestContext, HTTPRequestConcludingAsyncReader, HTTPRequestConcludingAsyncReader.Underlying, HTTPResponseConcludingAsyncWriter, diff --git a/Sources/HTTPServer/HTTPServerRequestHandler.swift b/Sources/HTTPServer/HTTPServerRequestHandler.swift index a5a61d3..39bc787 100644 --- a/Sources/HTTPServer/HTTPServerRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerRequestHandler.swift @@ -75,8 +75,6 @@ public protocol HTTPServerRequestHandler: Sendable { /// be `Span`. associatedtype RequestWriter: AsyncWriter, any Error> & ~Copyable - associatedtype RequestContext: HTTPRequestContext - /// Handles an incoming HTTP request and generates a response. /// /// This method is called by the HTTP server for each incoming client request. Implementations should: @@ -89,6 +87,7 @@ public protocol HTTPServerRequestHandler: Sendable { /// /// - 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. diff --git a/Sources/HTTPServer/NIOHTTPServer.swift b/Sources/HTTPServer/NIOHTTPServer.swift index 5f3e676..c5364d4 100644 --- a/Sources/HTTPServer/NIOHTTPServer.swift +++ b/Sources/HTTPServer/NIOHTTPServer.swift @@ -64,8 +64,7 @@ import Synchronization @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) public struct NIOHTTPServer: HTTPServerProtocol where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader, - RequestHandler.ConcludingResponseWriter == HTTPResponseConcludingAsyncWriter, - RequestHandler.RequestContext == HTTPServerRequestContext { + RequestHandler.ConcludingResponseWriter == HTTPResponseConcludingAsyncWriter { private let logger: Logger private let configuration: HTTPServerConfiguration @@ -425,7 +424,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader do { try await handler.handle( request: httpRequest, - requestContext: HTTPServerRequestContext(), + requestContext: HTTPRequestContext(), requestBodyAndTrailers: HTTPRequestConcludingAsyncReader( iterator: iterator, readerState: readerState diff --git a/Sources/HTTPServer/RequestResponseMiddlewareBox.swift b/Sources/HTTPServer/RequestResponseMiddlewareBox.swift index 8bf3a4e..28d6f42 100644 --- a/Sources/HTTPServer/RequestResponseMiddlewareBox.swift +++ b/Sources/HTTPServer/RequestResponseMiddlewareBox.swift @@ -4,12 +4,11 @@ public import HTTPTypes /// It is necessary to box them together so that they can be used with `Middlewares`, as this will be the `Middleware.Input`. @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) public struct RequestResponseMiddlewareBox< - RequestContext: HTTPRequestContext, RequestReader: ConcludingAsyncReader & ~Copyable, ResponseWriter: ConcludingAsyncWriter & ~Copyable >: ~Copyable { private let request: HTTPRequest - private let requestContext: RequestContext + private let requestContext: HTTPRequestContext private let requestReader: RequestReader private let responseSender: HTTPResponseSender @@ -20,7 +19,7 @@ public struct RequestResponseMiddlewareBox< /// - responseSender: The ``HTTPResponseSender``. public init( request: HTTPRequest, - requestContext: RequestContext, + requestContext: HTTPRequestContext, requestReader: consuming RequestReader, responseSender: consuming HTTPResponseSender ) { @@ -36,7 +35,7 @@ public struct RequestResponseMiddlewareBox< public consuming func withContents( _ handler: nonisolated(nonsending) ( HTTPRequest, - RequestContext, + HTTPRequestContext, consuming RequestReader, consuming HTTPResponseSender ) async throws -> T From c42fd4e5142db315cb249fd65488fe946ceca9c3 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 25 Nov 2025 11:59:41 +0000 Subject: [PATCH 4/6] Fix docs --- Sources/HTTPServer/HTTPResponseSender.swift | 2 +- .../HTTPServerClosureRequestHandler.swift | 53 +++++++++++++++++-- Sources/HTTPServer/HTTPServerProtocol.swift | 47 +--------------- .../HTTPServer/HTTPServerRequestHandler.swift | 13 ++--- 4 files changed, 58 insertions(+), 57 deletions(-) diff --git a/Sources/HTTPServer/HTTPResponseSender.swift b/Sources/HTTPServer/HTTPResponseSender.swift index eafa4a4..fe56c02 100644 --- a/Sources/HTTPServer/HTTPResponseSender.swift +++ b/Sources/HTTPServer/HTTPResponseSender.swift @@ -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. /// diff --git a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift index 5c78a26..f303ae9 100644 --- a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift @@ -8,7 +8,7 @@ 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 ... @@ -16,7 +16,7 @@ public import HTTPTypes /// /// // 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) @@ -34,7 +34,7 @@ public struct HTTPServerClosureRequestHandler< private let _handler: nonisolated(nonsending) @Sendable ( HTTPRequest, - RequestContext, + HTTPRequestContext, consuming sending HTTPRequestConcludingAsyncReader, consuming sending HTTPResponseSender ) async throws -> Void @@ -46,7 +46,7 @@ public struct HTTPServerClosureRequestHandler< public init( handler: nonisolated(nonsending) @Sendable @escaping ( HTTPRequest, - RequestContext, + HTTPRequestContext, consuming sending HTTPRequestConcludingAsyncReader, consuming sending HTTPResponseSender ) async throws -> Void @@ -65,10 +65,53 @@ public struct HTTPServerClosureRequestHandler< /// - responseSender: An ``HTTPResponseSender`` to send the HTTP response. public func handle( request: HTTPRequest, - requestContext: RequestContext, + requestContext: HTTPRequestContext, requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader, responseSender: consuming sending HTTPResponseSender ) 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 +> { + /// 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 + ) async throws -> Void + ) async throws { + try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler)) + } +} diff --git a/Sources/HTTPServer/HTTPServerProtocol.swift b/Sources/HTTPServer/HTTPServerProtocol.swift index eab09eb..3e4eadc 100644 --- a/Sources/HTTPServer/HTTPServerProtocol.swift +++ b/Sources/HTTPServer/HTTPServerProtocol.swift @@ -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 @@ -41,46 +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, - 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 - /// - ``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, - _ requestContext: HTTPServerRequestContext, - _ requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader, - _ responseSender: consuming sending 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 39bc787..f21a109 100644 --- a/Sources/HTTPServer/HTTPServerRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerRequestHandler.swift @@ -16,11 +16,12 @@ public import HTTPTypes /// /// ```swift /// struct EchoHandler: HTTPServerRequestHandler { -/// func handle( +/// func handle( /// request: HTTPRequest, -/// requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader, -/// responseSender: consuming HTTPResponseSender -/// ) async throws { +/// requestContext: HTTPRequestContext, +/// requestConcludingAsyncReader: consuming sending HTTPRequestConcludingAsyncReader, +/// responseSender: consuming sending HTTPResponseSender +/// ) async throws { /// // Read the entire request body /// let (bodyData, trailers) = try await requestConcludingAsyncReader.consumeAndConclude { reader in /// var reader = reader @@ -81,8 +82,8 @@ 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: From 49e08ec822bce7657415139ea94c080fb6c3e67f Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 25 Nov 2025 17:54:16 +0000 Subject: [PATCH 5/6] Remove implementation-specific property for now --- Sources/HTTPServer/HTTPRequestContext.swift | 37 ++------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/Sources/HTTPServer/HTTPRequestContext.swift b/Sources/HTTPServer/HTTPRequestContext.swift index 5201ede..9df1ea5 100644 --- a/Sources/HTTPServer/HTTPRequestContext.swift +++ b/Sources/HTTPServer/HTTPRequestContext.swift @@ -1,37 +1,4 @@ /// A context object that carries additional information about an HTTP request. /// -/// `HTTPRequestContext` provides a way to pass metadata and implementation-specific -/// information through the HTTP request pipeline. This is particularly useful when -/// you need to attach custom data that should be available throughout the request's -/// lifecycle. -/// -/// ## Implementation-Specific Data -/// -/// Different server implementations can store their own specific data by conforming to the -/// ``ImplementationSpecific`` protocol: -/// -/// ```swift -/// struct MyCustomContext: HTTPRequestContext.ImplementationSpecific { -/// let requestID: UUID -/// let startTime: Date -/// } -/// -/// var context = HTTPRequestContext() -/// context.implementationSpecific = MyCustomContext( -/// requestID: UUID(), -/// startTime: Date() -/// ) -/// ``` -public struct HTTPRequestContext: Sendable { - - /// Conform your custom types to this protocol to store implementation-specific - /// data within an ``HTTPRequestContext``. - public protocol ImplementationSpecific: Sendable {} - - /// Optional implementation-specific data associated with this request context. - /// - /// Use this property to store custom data that is specific to your HTTP - /// implementation. The stored value can be any type that conforms to - /// ``ImplementationSpecific``. - public var implementationSpecific: (any ImplementationSpecific)? -} +/// `HTTPRequestContext` provides a way to pass metadata through the HTTP request pipeline. +public struct HTTPRequestContext: Sendable {} From 361a62465d4794efadddf4ccbac178c85dae3f57 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 26 Nov 2025 11:14:34 +0000 Subject: [PATCH 6/6] Fix rebase --- Sources/HTTPServer/HTTPServerRequestHandler.swift | 2 +- Tests/HTTPServerTests/HTTPServerTests.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/HTTPServer/HTTPServerRequestHandler.swift b/Sources/HTTPServer/HTTPServerRequestHandler.swift index f21a109..4cdbe68 100644 --- a/Sources/HTTPServer/HTTPServerRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerRequestHandler.swift @@ -98,7 +98,7 @@ public protocol HTTPServerRequestHandler: Sendable { /// - Throws: Any error encountered during request processing or response generation. func handle( request: HTTPRequest, - requestContext: RequestContext, + requestContext: HTTPRequestContext, requestBodyAndTrailers: consuming sending ConcludingRequestReader, responseSender: consuming sending HTTPResponseSender ) async throws diff --git a/Tests/HTTPServerTests/HTTPServerTests.swift b/Tests/HTTPServerTests/HTTPServerTests.swift index 64f78d5..21501eb 100644 --- a/Tests/HTTPServerTests/HTTPServerTests.swift +++ b/Tests/HTTPServerTests/HTTPServerTests.swift @@ -9,7 +9,6 @@ struct HTTPServerTests { @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) func testConsumingServe() async throws { let server = NIOHTTPServer