diff --git a/Examples/ExampleMiddleware/HTTPServerLoggingMiddleware.swift b/Examples/ExampleMiddleware/HTTPServerLoggingMiddleware.swift index 5c8929d..a0d2524 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(anyAppleOS 26.0, *) 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 cbb0ae2..f01dcdb 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(anyAppleOS 26.0, *) 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 ebc715b..9df379a 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(anyAppleOS 26.0, *) 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 15b8c34..848544f 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/HTTPServer.swift b/Sources/HTTPAPIs/Server/HTTPServer.swift index e36b52d..9fe9ded 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, ~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 associatedtype RequestConcludingReader: ConcludingAsyncReader, ~Copyable, SendableMetatype @@ -52,6 +54,8 @@ public protocol HTTPServer: S /// ``` func serve(handler: Handler) async throws where + Handler.RequestContext: ~Copyable & ~Escapable, + 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 ef0e397..9a84ea2 100644 --- a/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift @@ -36,6 +36,7 @@ /// ``` @available(anyAppleOS 26.0, *) public struct HTTPServerClosureRequestHandler< + RequestContext: HTTPServerCapability.RequestContext & ~Copyable & ~Escapable, 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, + consuming RequestContext, consuming sending RequestReader, consuming sending HTTPResponseSender ) async throws -> Void @@ -65,7 +67,7 @@ where handler: @Sendable @escaping ( HTTPRequest, - HTTPRequestContext, + consuming 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: consuming 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: 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 a4db05d..7a9c8b0 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(anyAppleOS 26.0, *) -public protocol HTTPServerRequestHandler: Sendable { +public protocol HTTPServerRequestHandler: Sendable { + /// The type of the request context provided by the server. + associatedtype RequestContext: HTTPServerCapability.RequestContext, ~Copyable, ~Escapable + /// 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: consuming RequestContext, requestBodyAndTrailers: consuming sending RequestReader, responseSender: consuming sending HTTPResponseSender ) async throws diff --git a/Sources/HTTPAPIs/Server/HTTPRequestContext.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/HTTPRequestContext.swift similarity index 68% rename from Sources/HTTPAPIs/Server/HTTPRequestContext.swift rename to Sources/HTTPClientConformance/HTTPServerForTesting/HTTPRequestContext.swift index 63e2909..325d48f 100644 --- a/Sources/HTTPAPIs/Server/HTTPRequestContext.swift +++ b/Sources/HTTPClientConformance/HTTPServerForTesting/HTTPRequestContext.swift @@ -11,9 +11,9 @@ // //===----------------------------------------------------------------------===// -/// 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 { +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() {} } diff --git a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+HTTP1_1.swift b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer+HTTP1_1.swift index 2cb4643..c80c263 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 f9e38ad..0588d12 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 950e512..c40bd9a 100644 --- a/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer.swift +++ b/Sources/HTTPClientConformance/HTTPServerForTesting/NIOHTTPServer.swift @@ -78,6 +78,7 @@ import X509 /// ``` @available(anyAppleOS 26.0, *) 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 0e6a1c7..7966216 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(anyAppleOS 26.0, *) 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 624c07f..85efc47 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) + } + +}