From 9d24d2323eec3ed33ebe6ebdd810845fd5a78ca7 Mon Sep 17 00:00:00 2001 From: Eric Rosenberg Date: Tue, 19 May 2026 20:24:39 +0000 Subject: [PATCH 1/2] Add request context capability mechanism to HTTPServer Introduce HTTPServerCapability namespace with a base RequestContext protocol, mirroring the client-side HTTPClientCapability.RequestOptions pattern. Servers declare a RequestContext associated type that handlers can constrain to require specific capabilities (e.g. connection info, peer certificates) at compile time. --- .../HTTPServerLoggingMiddleware.swift | 11 ++- .../HTTPServerMiddlewareInput.swift | 7 +- .../HTTPServerRequestHandlerMiddleware.swift | 8 +- .../ExampleMiddlewareServer.swift | 4 +- .../HTTPAPIs/Server/HTTPRequestContext.swift | 7 +- Sources/HTTPAPIs/Server/HTTPServer.swift | 5 +- .../HTTPServerCapability+RequestContext.swift | 23 +++++ .../HTTPServerClosureRequestHandler.swift | 16 ++-- .../Server/HTTPServerRequestHandler.swift | 12 ++- .../NIOHTTPServer+HTTP1_1.swift | 4 +- .../NIOHTTPServer+SecureUpgrade.swift | 4 +- .../HTTPServerForTesting/NIOHTTPServer.swift | 5 +- .../RequestResponseMiddlewareBox.swift | 8 +- .../Helpers/HTTPClientAndServerTests.swift | 5 +- .../HTTPAPIsTests/ServerCapabilityTests.swift | 94 +++++++++++++++++++ 15 files changed, 174 insertions(+), 39 deletions(-) create mode 100644 Sources/HTTPAPIs/Server/HTTPServerCapability+RequestContext.swift create mode 100644 Tests/HTTPAPIsTests/ServerCapabilityTests.swift diff --git a/Examples/ExampleMiddleware/HTTPServerLoggingMiddleware.swift b/Examples/ExampleMiddleware/HTTPServerLoggingMiddleware.swift index 5d1bbee..a7fac5d 100644 --- a/Examples/ExampleMiddleware/HTTPServerLoggingMiddleware.swift +++ b/Examples/ExampleMiddleware/HTTPServerLoggingMiddleware.swift @@ -23,6 +23,7 @@ public import Middleware /// This middleware is useful for debugging and monitoring HTTP traffic. @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) public struct HTTPServerLoggingMiddleware< + RequestContext: HTTPServerCapability.RequestContext, RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable, ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable >: Middleware @@ -36,8 +37,9 @@ where ResponseConcludingAsyncWriter.Underlying.WriteElement == UInt8, ResponseConcludingAsyncWriter.FinalElement == HTTPFields? { - public typealias Input = HTTPServerMiddlewareInput + public typealias Input = HTTPServerMiddlewareInput public typealias NextInput = HTTPServerMiddlewareInput< + RequestContext, HTTPRequestLoggingConcludingAsyncReader, HTTPResponseLoggingConcludingAsyncWriter > @@ -117,11 +119,12 @@ extension Middleware where Input: ~Copyable, NextInput: ~Copyable { /// .requestHandler() /// } /// ``` - public func logging( + public func logging( logger: Logger - ) -> HTTPServerLoggingMiddleware + ) -> HTTPServerLoggingMiddleware where - Input == HTTPServerMiddlewareInput, + Input == HTTPServerMiddlewareInput, + RequestContext: HTTPServerCapability.RequestContext, RequestReader: ConcludingAsyncReader & ~Copyable & Escapable, RequestReader.Underlying: ~Copyable & Escapable, RequestReader.Underlying.ReadElement == UInt8, diff --git a/Examples/ExampleMiddleware/HTTPServerMiddlewareInput.swift b/Examples/ExampleMiddleware/HTTPServerMiddlewareInput.swift index 5ecf49e..bb1cac6 100644 --- a/Examples/ExampleMiddleware/HTTPServerMiddlewareInput.swift +++ b/Examples/ExampleMiddleware/HTTPServerMiddlewareInput.swift @@ -21,11 +21,12 @@ public import HTTPAPIs /// convenient way to pass all request-handling components through the middleware chain. @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) public struct HTTPServerMiddlewareInput< + RequestContext: HTTPServerCapability.RequestContext, RequestReader: ConcludingAsyncReader & ~Copyable, ResponseWriter: ConcludingAsyncWriter & ~Copyable >: ~Copyable where RequestReader.Underlying: ~Copyable, ResponseWriter.Underlying: ~Copyable { private let request: HTTPRequest - private let requestContext: HTTPRequestContext + private let requestContext: RequestContext private let requestReader: RequestReader private let responseSender: HTTPResponseSender @@ -38,7 +39,7 @@ public struct HTTPServerMiddlewareInput< /// - responseSender: A sender for transmitting the HTTP response and response body. public init( request: HTTPRequest, - requestContext: HTTPRequestContext, + requestContext: RequestContext, requestReader: consuming RequestReader, responseSender: consuming HTTPResponseSender ) { @@ -63,7 +64,7 @@ public struct HTTPServerMiddlewareInput< _ handler: ( HTTPRequest, - HTTPRequestContext, + RequestContext, consuming RequestReader, consuming HTTPResponseSender ) async throws -> Return diff --git a/Examples/ExampleMiddleware/HTTPServerRequestHandlerMiddleware.swift b/Examples/ExampleMiddleware/HTTPServerRequestHandlerMiddleware.swift index b8f3ea3..177420b 100644 --- a/Examples/ExampleMiddleware/HTTPServerRequestHandlerMiddleware.swift +++ b/Examples/ExampleMiddleware/HTTPServerRequestHandlerMiddleware.swift @@ -21,6 +21,7 @@ public import Middleware /// This middleware has `Never` as its `NextInput` type, indicating it's the end of the chain. @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) public struct HTTPServerRequestHandlerMiddleware< + RequestContext: HTTPServerCapability.RequestContext, RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable, ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable, >: Middleware, Sendable @@ -32,7 +33,7 @@ where ResponseConcludingAsyncWriter.Underlying.WriteElement == UInt8, ResponseConcludingAsyncWriter.FinalElement == HTTPFields? { - public typealias Input = HTTPServerMiddlewareInput + public typealias Input = HTTPServerMiddlewareInput public typealias NextInput = Void /// Creates a new request handler middleware. @@ -82,9 +83,10 @@ extension Middleware where Input: ~Copyable, NextInput: ~Copyable { /// .requestHandler() /// } /// ``` - public func requestHandler() -> HTTPServerRequestHandlerMiddleware + public func requestHandler() -> HTTPServerRequestHandlerMiddleware where - Input == HTTPServerMiddlewareInput, + Input == HTTPServerMiddlewareInput, + RequestContext: HTTPServerCapability.RequestContext, RequestReader: ConcludingAsyncReader & ~Copyable, RequestReader.Underlying: ~Copyable, RequestReader.Underlying.ReadElement == UInt8, diff --git a/Examples/MiddlewareServer/ExampleMiddlewareServer.swift b/Examples/MiddlewareServer/ExampleMiddlewareServer.swift index c0ca7ce..2245b45 100644 --- a/Examples/MiddlewareServer/ExampleMiddlewareServer.swift +++ b/Examples/MiddlewareServer/ExampleMiddlewareServer.swift @@ -29,7 +29,7 @@ where Server.ResponseConcludingWriter.Underlying: ~Copyable, ServerMiddleware.Input: ~Copyable, ServerMiddleware.NextInput: ~Copyable, - ServerMiddleware.Input == HTTPServerMiddlewareInput + ServerMiddleware.Input == HTTPServerMiddlewareInput { typealias RequestConcludingReader = Server.RequestConcludingReader typealias ResponseConcludingWriter = Server.ResponseConcludingWriter @@ -70,7 +70,7 @@ where Server.ResponseConcludingWriter: ~Copyable, Server.ResponseConcludingWriter.Underlying: ~Copyable { - typealias Input = HTTPServerMiddlewareInput + typealias Input = HTTPServerMiddlewareInput typealias NextInput = Input func intercept( diff --git a/Sources/HTTPAPIs/Server/HTTPRequestContext.swift b/Sources/HTTPAPIs/Server/HTTPRequestContext.swift index 63e2909..b820be7 100644 --- a/Sources/HTTPAPIs/Server/HTTPRequestContext.swift +++ b/Sources/HTTPAPIs/Server/HTTPRequestContext.swift @@ -11,9 +11,8 @@ // //===----------------------------------------------------------------------===// -/// 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 { +/// The default request context for HTTP server implementations. +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +public struct HTTPRequestContext: HTTPServerCapability.RequestContext { public init() {} } diff --git a/Sources/HTTPAPIs/Server/HTTPServer.swift b/Sources/HTTPAPIs/Server/HTTPServer.swift index 0679a2a..c4120d7 100644 --- a/Sources/HTTPAPIs/Server/HTTPServer.swift +++ b/Sources/HTTPAPIs/Server/HTTPServer.swift @@ -16,7 +16,9 @@ /// /// ``HTTPServer`` provides the contract for server implementations that accept /// incoming HTTP connections and process requests using a ``HTTPServerRequestHandler``. -public protocol HTTPServer: Sendable, ~Copyable, ~Escapable { +public protocol HTTPServer: Sendable, ~Copyable, ~Escapable { + associatedtype RequestContext: HTTPServerCapability.RequestContext + /// The type used to read request body data and trailers. // TODO: Check if we should allow ~Escapable readers https://github.com/apple/swift-http-api-proposal/issues/13 associatedtype RequestConcludingReader: ConcludingAsyncReader, ~Copyable, SendableMetatype @@ -52,6 +54,7 @@ public protocol HTTPServer: S /// ``` func serve(handler: Handler) async throws where + Handler.RequestContext == RequestContext, Handler.RequestReader == RequestConcludingReader, Handler.RequestReader: ~Copyable, Handler.ResponseWriter == ResponseConcludingWriter, diff --git a/Sources/HTTPAPIs/Server/HTTPServerCapability+RequestContext.swift b/Sources/HTTPAPIs/Server/HTTPServerCapability+RequestContext.swift new file mode 100644 index 0000000..314c068 --- /dev/null +++ b/Sources/HTTPAPIs/Server/HTTPServerCapability+RequestContext.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP API Proposal open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// The namespace for all protocols defining HTTP server capabilities. +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +public enum HTTPServerCapability { + /// The request context protocol. + /// + /// Child protocols define additional context that a subset of servers provide, + /// allowing libraries to depend on specific capabilities. + public protocol RequestContext: ~Copyable, ~Escapable { + } +} diff --git a/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift b/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift index dbe5e44..c48350f 100644 --- a/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift @@ -36,6 +36,7 @@ /// ``` @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) public struct HTTPServerClosureRequestHandler< + RequestContext: HTTPServerCapability.RequestContext, RequestReader: ConcludingAsyncReader & ~Copyable, ResponseWriter: ConcludingAsyncWriter & ~Copyable, >: HTTPServerRequestHandler @@ -47,11 +48,12 @@ where RequestReader.FinalElement == HTTPFields?, ResponseWriter.FinalElement == HTTPFields? { + /// The underlying closure that handles HTTP requests. private let _handler: @Sendable ( HTTPRequest, - HTTPRequestContext, + RequestContext, consuming sending RequestReader, consuming sending HTTPResponseSender ) async throws -> Void @@ -65,7 +67,7 @@ where handler: @Sendable @escaping ( HTTPRequest, - HTTPRequestContext, + RequestContext, consuming sending RequestReader, consuming sending HTTPResponseSender ) async throws -> Void @@ -79,12 +81,12 @@ where /// /// - Parameters: /// - request: The HTTP request headers and metadata. - /// - requestContext: A ``HTTPRequestContext``. + /// - requestContext: The request context provided by the server. /// - 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, + requestContext: RequestContext, requestBodyAndTrailers: consuming sending RequestReader, responseSender: consuming sending HTTPResponseSender ) async throws { @@ -109,14 +111,14 @@ where /// - 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. + /// - `RequestContext`: The request context provided by the server. /// - ``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 + /// try await server.serve { request, requestContext, bodyReader, responseSender in /// // Process the request /// let response = HTTPResponse(status: .ok) /// let writer = try await responseSender.send(response) @@ -130,7 +132,7 @@ where handler: @Sendable @escaping ( _ request: HTTPRequest, - _ requestContext: HTTPRequestContext, + _ requestContext: RequestContext, _ requestBodyAndTrailers: consuming sending RequestConcludingReader, _ responseSender: consuming sending HTTPResponseSender ) async throws -> Void diff --git a/Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift b/Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift index 28e3c92..e26061a 100644 --- a/Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift +++ b/Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift @@ -26,6 +26,7 @@ /// /// ```swift /// struct EchoHandler< +/// Context: HTTPServerCapability.RequestContext, /// ConcludingRequestReader: ConcludingAsyncReader & ~Copyable, /// RequestReader: AsyncReader & ~Copyable, /// ConcludingResponseWriter: ConcludingAsyncWriter & ~Copyable, @@ -33,7 +34,7 @@ /// >: HTTPServerRequestHandler { /// func handle( /// request: HTTPRequest, -/// requestContext: HTTPRequestContext, +/// requestContext: Context, /// requestBodyAndTrailers: consuming sending ConcludingRequestReader, /// responseSender: consuming sending HTTPResponseSender /// ) async throws { @@ -55,7 +56,10 @@ /// } /// ``` @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) -public protocol HTTPServerRequestHandler: Sendable { +public protocol HTTPServerRequestHandler: Sendable { + /// The type of the request context provided by the server. + associatedtype RequestContext: HTTPServerCapability.RequestContext + /// The type used to read request body data and trailers. associatedtype RequestReader: ConcludingAsyncReader, ~Copyable where RequestReader.Underlying: ~Copyable, RequestReader.Underlying.ReadElement == UInt8, RequestReader.FinalElement == HTTPFields? @@ -76,7 +80,7 @@ public protocol HTTPServerRequestHandler: Sendabl /// /// - Parameters: /// - request: The HTTP request headers and metadata. - /// - requestContext: A ``HTTPRequestContext`` carrying additional request information. + /// - requestContext: A context carrying additional request information provided by the server. /// - requestBodyAndTrailers: A reader for accessing the request body data and trailing headers. /// - responseSender: An ``HTTPResponseSender`` that accepts an HTTP response and returns a writer for the /// response body. The returned writer allows for incremental writing of the response body and supports trailers. @@ -84,7 +88,7 @@ public protocol HTTPServerRequestHandler: Sendabl /// - Throws: Any error encountered during request processing or response generation. func handle( request: HTTPRequest, - requestContext: HTTPRequestContext, + requestContext: RequestContext, requestBodyAndTrailers: consuming sending RequestReader, responseSender: consuming sending HTTPResponseSender ) async throws diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+HTTP1_1.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+HTTP1_1.swift index 8bffc4f..ec6f49f 100644 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+HTTP1_1.swift +++ b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+HTTP1_1.swift @@ -23,7 +23,7 @@ import NIOPosix extension NIOHTTPServer { func serveInsecureHTTP1_1( bindTarget: NIOHTTPServerConfiguration.BindTarget, - handler: some HTTPServerRequestHandler, + handler: some HTTPServerRequestHandler, asyncChannelConfiguration: NIOAsyncChannel.Configuration ) async throws { let serverChannel = try await self.setupHTTP1_1ServerChannel( @@ -71,7 +71,7 @@ extension NIOHTTPServer { func _serveInsecureHTTP1_1( serverChannel: NIOAsyncChannel, Never>, - handler: some HTTPServerRequestHandler + handler: some HTTPServerRequestHandler ) async throws { try await withThrowingDiscardingTaskGroup { group in try await serverChannel.executeThenClose { inbound in diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SecureUpgrade.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SecureUpgrade.swift index a46b3a5..ce3875f 100644 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SecureUpgrade.swift +++ b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+SecureUpgrade.swift @@ -28,7 +28,7 @@ extension NIOHTTPServer { func serveSecureUpgrade( bindTarget: NIOHTTPServerConfiguration.BindTarget, tlsConfiguration: TLSConfiguration, - handler: some HTTPServerRequestHandler, + handler: some HTTPServerRequestHandler, asyncChannelConfiguration: NIOAsyncChannel.Configuration, http2Configuration: NIOHTTP2Handler.Configuration, verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? = nil @@ -120,7 +120,7 @@ extension NIOHTTPServer { func _serveSecureUpgrade( serverChannel: NIOAsyncChannel, Never>, - handler: some HTTPServerRequestHandler + handler: some HTTPServerRequestHandler ) async throws { try await withThrowingDiscardingTaskGroup { group in try await serverChannel.executeThenClose { inbound in diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer.swift index 416728a..2c825ca 100644 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer.swift +++ b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer.swift @@ -78,6 +78,7 @@ import X509 /// ``` @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) public struct NIOHTTPServer: HTTPServer { + public typealias RequestContext = HTTPRequestContext public typealias RequestConcludingReader = HTTPRequestConcludingAsyncReader public typealias ResponseConcludingWriter = HTTPResponseConcludingAsyncWriter @@ -145,7 +146,7 @@ public struct NIOHTTPServer: HTTPServer { /// handler: EchoHandler() /// ) /// ``` - public func serve(handler: some HTTPServerRequestHandler) async throws { + public func serve(handler: some HTTPServerRequestHandler) async throws { defer { switch self.listeningAddressState.withLockedValue({ $0.close() }) { case .failPromise(let promise, let error): @@ -265,7 +266,7 @@ public struct NIOHTTPServer: HTTPServer { func handleRequestChannel( channel: NIOAsyncChannel, - handler: some HTTPServerRequestHandler + handler: some HTTPServerRequestHandler ) async throws { do { try await channel diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/RequestResponseMiddlewareBox.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/RequestResponseMiddlewareBox.swift index 473f183..5373fff 100644 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/RequestResponseMiddlewareBox.swift +++ b/Sources/HTTPClientConformance/HTTPServerForTesting/RequestResponseMiddlewareBox.swift @@ -19,22 +19,24 @@ 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.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) public struct RequestResponseMiddlewareBox< + RequestContext: HTTPServerCapability.RequestContext, RequestReader: ConcludingAsyncReader & ~Copyable, ResponseWriter: ConcludingAsyncWriter & ~Copyable >: ~Copyable { private let request: HTTPRequest - private let requestContext: HTTPRequestContext + private let requestContext: RequestContext private let requestReader: RequestReader private let responseSender: HTTPResponseSender /// Create a new ``RequestResponseMiddlewareBox``. /// - Parameters: /// - request: The `HTTPRequest`. + /// - requestContext: The request context. /// - requestReader: The `RequestReader`. /// - responseSender: The ``HTTPResponseSender``. public init( request: HTTPRequest, - requestContext: HTTPRequestContext, + requestContext: RequestContext, requestReader: consuming RequestReader, responseSender: consuming HTTPResponseSender ) { @@ -51,7 +53,7 @@ public struct RequestResponseMiddlewareBox< _ handler: nonisolated(nonsending) ( HTTPRequest, - HTTPRequestContext, + RequestContext, consuming RequestReader, consuming HTTPResponseSender ) async throws -> T diff --git a/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift b/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift index bd0e98b..b675c7e 100644 --- a/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift +++ b/Tests/HTTPAPIsTests/Helpers/HTTPClientAndServerTests.swift @@ -120,6 +120,7 @@ final class TestClientAndServer: HTTPClient, HTTPServer { typealias RequestWriter = AsyncChannelConcludingAsyncWriter.Underlying typealias ResponseConcludingReader = AsyncChannelConcludingAsyncReader + typealias RequestContext = HTTPRequestContext typealias RequestConcludingReader = AsyncChannelConcludingAsyncReader typealias ResponseConcludingWriter = AsyncChannelConcludingAsyncWriter @@ -164,7 +165,7 @@ final class TestClientAndServer: HTTPClient, HTTPServer { } func serve( - handler: some HTTPServerRequestHandler + handler: some HTTPServerRequestHandler ) async throws { try await withThrowingDiscardingTaskGroup { group in for await _ in self.stream { @@ -184,7 +185,7 @@ final class TestClientAndServer: HTTPClient, HTTPServer { private static func handleRequest( request: consuming BufferedRequest, - handler: some HTTPServerRequestHandler + handler: some HTTPServerRequestHandler ) async throws { try await withThrowingTaskGroup { group in let trailersChannel = AsyncChannel() diff --git a/Tests/HTTPAPIsTests/ServerCapabilityTests.swift b/Tests/HTTPAPIsTests/ServerCapabilityTests.swift new file mode 100644 index 0000000..bc8a1ad --- /dev/null +++ b/Tests/HTTPAPIsTests/ServerCapabilityTests.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift HTTP API Proposal open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import HTTPAPIs +import Testing + +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +extension HTTPServerCapability { + protocol ConnectionInfo: RequestContext { + var remoteAddress: String? { get } + var localAddress: String? { get } + var negotiatedProtocol: String? { get } + } +} + +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +struct TestConnectionContext: HTTPServerCapability.ConnectionInfo { + var remoteAddress: String? + var localAddress: String? + var negotiatedProtocol: String? +} + +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +func connectionInfoHandler( + server: S +) async throws +where + S.RequestContext: HTTPServerCapability.ConnectionInfo, + S.RequestConcludingReader: ~Copyable, + S.RequestConcludingReader.Underlying: ~Copyable, + S.ResponseConcludingWriter: ~Copyable, + S.ResponseConcludingWriter.Underlying: ~Copyable +{ + try await server.serve { request, requestContext, requestBodyAndTrailers, responseSender in + let remote = requestContext.remoteAddress ?? "unknown" + let responseBodyAndTrailers = try await responseSender.send(.init(status: .ok)) + try await responseBodyAndTrailers.writeAndConclude(remote.utf8.span, finalElement: nil) + } +} + +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) +struct ConnectionInfoTestServer: HTTPServer { + typealias RequestContext = TestConnectionContext + typealias RequestConcludingReader = TestClientAndServer.AsyncChannelConcludingAsyncReader + typealias ResponseConcludingWriter = TestClientAndServer.AsyncChannelConcludingAsyncWriter + + let context: TestConnectionContext + + init(context: TestConnectionContext) { + self.context = context + } + + func serve(handler: Handler) async throws + where + Handler.RequestContext == TestConnectionContext, + Handler.RequestReader == RequestConcludingReader, + Handler.RequestReader: ~Copyable, + Handler.ResponseWriter == ResponseConcludingWriter, + Handler.ResponseWriter: ~Copyable + { + // This test just verifies the capability constraint compiles and the + // context is accessible. A full integration test would wire up connections. + } +} + +@Suite("Server Capability Tests") +struct ServerCapabilityTests { + @Test("ConnectionInfo capability constraint compiles and context is accessible") + @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) + func connectionInfoCapability() async throws { + let context = TestConnectionContext( + remoteAddress: "127.0.0.1:54321", + localAddress: "0.0.0.0:8080", + negotiatedProtocol: "h2" + ) + + // Verify a server with ConnectionInfo context can be passed to a + // function requiring that capability + let server = ConnectionInfoTestServer(context: context) + try await connectionInfoHandler(server: server) + } + +} From cdbb0c6227070c9bd11118f2e60f53600af4b241 Mon Sep 17 00:00:00 2001 From: Eric Rosenberg Date: Thu, 21 May 2026 03:05:03 +0000 Subject: [PATCH 2/2] Address PR feedback: add ~Copyable, ~Escapable to RequestContext - Add ~Copyable, ~Escapable constraints to RequestContext associated types in HTTPServer, HTTPServerRequestHandler, and HTTPServerClosureRequestHandler - Add consuming ownership to requestContext parameters - Move HTTPRequestContext concrete type out of HTTPAPIs into the test server module --- Sources/HTTPAPIs/Server/HTTPServer.swift | 3 ++- .../Server/HTTPServerClosureRequestHandler.swift | 10 +++++----- Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift | 4 ++-- .../HTTPServerForTesting}/HTTPRequestContext.swift | 3 ++- 4 files changed, 11 insertions(+), 9 deletions(-) rename Sources/{HTTPAPIs/Server => HTTPClientConformance/HTTPServerForTesting}/HTTPRequestContext.swift (90%) diff --git a/Sources/HTTPAPIs/Server/HTTPServer.swift b/Sources/HTTPAPIs/Server/HTTPServer.swift index c4120d7..40da56d 100644 --- a/Sources/HTTPAPIs/Server/HTTPServer.swift +++ b/Sources/HTTPAPIs/Server/HTTPServer.swift @@ -17,7 +17,7 @@ /// ``HTTPServer`` provides the contract for server implementations that accept /// incoming HTTP connections and process requests using a ``HTTPServerRequestHandler``. public protocol HTTPServer: Sendable, ~Copyable, ~Escapable { - associatedtype RequestContext: HTTPServerCapability.RequestContext + associatedtype RequestContext: HTTPServerCapability.RequestContext, ~Copyable, ~Escapable /// The type used to read request body data and trailers. // TODO: Check if we should allow ~Escapable readers https://github.com/apple/swift-http-api-proposal/issues/13 @@ -54,6 +54,7 @@ public protocol HTTPServer(handler: Handler) async throws where + Handler.RequestContext: ~Copyable & ~Escapable, Handler.RequestContext == RequestContext, Handler.RequestReader == RequestConcludingReader, Handler.RequestReader: ~Copyable, diff --git a/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift b/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift index c48350f..e4345d1 100644 --- a/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift @@ -36,7 +36,7 @@ /// ``` @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) public struct HTTPServerClosureRequestHandler< - RequestContext: HTTPServerCapability.RequestContext, + RequestContext: HTTPServerCapability.RequestContext & ~Copyable & ~Escapable, RequestReader: ConcludingAsyncReader & ~Copyable, ResponseWriter: ConcludingAsyncWriter & ~Copyable, >: HTTPServerRequestHandler @@ -53,7 +53,7 @@ where private let _handler: @Sendable ( HTTPRequest, - RequestContext, + consuming RequestContext, consuming sending RequestReader, consuming sending HTTPResponseSender ) async throws -> Void @@ -67,7 +67,7 @@ where handler: @Sendable @escaping ( HTTPRequest, - RequestContext, + consuming RequestContext, consuming sending RequestReader, consuming sending HTTPResponseSender ) async throws -> Void @@ -86,7 +86,7 @@ where /// - responseSender: An ``HTTPResponseSender`` to send the HTTP response. public func handle( request: HTTPRequest, - requestContext: RequestContext, + requestContext: consuming RequestContext, requestBodyAndTrailers: consuming sending RequestReader, responseSender: consuming sending HTTPResponseSender ) async throws { @@ -132,7 +132,7 @@ where handler: @Sendable @escaping ( _ request: HTTPRequest, - _ requestContext: RequestContext, + _ requestContext: consuming RequestContext, _ requestBodyAndTrailers: consuming sending RequestConcludingReader, _ responseSender: consuming sending HTTPResponseSender ) async throws -> Void diff --git a/Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift b/Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift index e26061a..779876d 100644 --- a/Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift +++ b/Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift @@ -58,7 +58,7 @@ @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) public protocol HTTPServerRequestHandler: Sendable { /// The type of the request context provided by the server. - associatedtype RequestContext: HTTPServerCapability.RequestContext + associatedtype RequestContext: HTTPServerCapability.RequestContext, ~Copyable, ~Escapable /// The type used to read request body data and trailers. associatedtype RequestReader: ConcludingAsyncReader, ~Copyable @@ -88,7 +88,7 @@ public protocol HTTPServerRequestHandler ) async throws diff --git a/Sources/HTTPAPIs/Server/HTTPRequestContext.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/HTTPRequestContext.swift similarity index 90% rename from Sources/HTTPAPIs/Server/HTTPRequestContext.swift rename to Sources/HTTPClientConformance/HTTPServerForTesting/HTTPRequestContext.swift index b820be7..325d48f 100644 --- a/Sources/HTTPAPIs/Server/HTTPRequestContext.swift +++ b/Sources/HTTPClientConformance/HTTPServerForTesting/HTTPRequestContext.swift @@ -11,7 +11,8 @@ // //===----------------------------------------------------------------------===// -/// The default request context for HTTP server implementations. +public import HTTPAPIs + @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) public struct HTTPRequestContext: HTTPServerCapability.RequestContext { public init() {}