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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions Examples/ExampleMiddleware/HTTPServerLoggingMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 & ~Copyable,
RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable,
ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable
>: Middleware
Expand All @@ -36,8 +37,9 @@ where
ResponseConcludingAsyncWriter.Underlying.WriteElement == UInt8,
ResponseConcludingAsyncWriter.FinalElement == HTTPFields?
{
public typealias Input = HTTPServerMiddlewareInput<RequestConcludingAsyncReader, ResponseConcludingAsyncWriter>
public typealias Input = HTTPServerMiddlewareInput<RequestContext, RequestConcludingAsyncReader, ResponseConcludingAsyncWriter>
public typealias NextInput = HTTPServerMiddlewareInput<
RequestContext,
HTTPRequestLoggingConcludingAsyncReader<RequestConcludingAsyncReader>,
HTTPResponseLoggingConcludingAsyncWriter<ResponseConcludingAsyncWriter>
>
Expand Down Expand Up @@ -117,11 +119,12 @@ extension Middleware where Input: ~Copyable, NextInput: ~Copyable {
/// .requestHandler()
/// }
/// ```
public func logging<RequestReader, ResponseWriter>(
public func logging<RequestContext, RequestReader, ResponseWriter>(
logger: Logger
) -> HTTPServerLoggingMiddleware<RequestReader, ResponseWriter>
) -> HTTPServerLoggingMiddleware<RequestContext, RequestReader, ResponseWriter>
where
Input == HTTPServerMiddlewareInput<RequestReader, ResponseWriter>,
Input == HTTPServerMiddlewareInput<RequestContext, RequestReader, ResponseWriter>,
RequestContext: HTTPServerCapability.RequestContext & ~Copyable,
RequestReader: ConcludingAsyncReader & ~Copyable & Escapable,
RequestReader.Underlying: ~Copyable & Escapable,
RequestReader.Underlying.ReadElement == UInt8,
Expand Down
7 changes: 4 additions & 3 deletions Examples/ExampleMiddleware/HTTPServerMiddlewareInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 & ~Copyable,
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<ResponseWriter>

Expand All @@ -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: consuming RequestContext,
requestReader: consuming RequestReader,
responseSender: consuming HTTPResponseSender<ResponseWriter>
) {
Expand All @@ -63,7 +64,7 @@ public struct HTTPServerMiddlewareInput<
_ handler:
(
HTTPRequest,
HTTPRequestContext,
consuming RequestContext,
consuming RequestReader,
consuming HTTPResponseSender<ResponseWriter>
) async throws -> Return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 & ~Copyable,
RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable,
ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable,
>: Middleware, Sendable
Expand All @@ -32,7 +33,7 @@ where
ResponseConcludingAsyncWriter.Underlying.WriteElement == UInt8,
ResponseConcludingAsyncWriter.FinalElement == HTTPFields?
{
public typealias Input = HTTPServerMiddlewareInput<RequestConcludingAsyncReader, ResponseConcludingAsyncWriter>
public typealias Input = HTTPServerMiddlewareInput<RequestContext, RequestConcludingAsyncReader, ResponseConcludingAsyncWriter>
public typealias NextInput = Void

/// Creates a new request handler middleware.
Expand Down Expand Up @@ -82,9 +83,12 @@ extension Middleware where Input: ~Copyable, NextInput: ~Copyable {
/// .requestHandler()
/// }
/// ```
public func requestHandler<RequestReader, ResponseWriter>() -> HTTPServerRequestHandlerMiddleware<RequestReader, ResponseWriter>
public func requestHandler<RequestContext, RequestReader, ResponseWriter>() -> HTTPServerRequestHandlerMiddleware<
RequestContext, RequestReader, ResponseWriter
>
where
Input == HTTPServerMiddlewareInput<RequestReader, ResponseWriter>,
Input == HTTPServerMiddlewareInput<RequestContext, RequestReader, ResponseWriter>,
RequestContext: HTTPServerCapability.RequestContext & ~Copyable,
RequestReader: ConcludingAsyncReader & ~Copyable,
RequestReader.Underlying: ~Copyable,
RequestReader.Underlying.ReadElement == UInt8,
Expand Down
4 changes: 2 additions & 2 deletions Examples/MiddlewareServer/ExampleMiddlewareServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ where
Server.ResponseConcludingWriter.Underlying: ~Copyable,
ServerMiddleware.Input: ~Copyable,
ServerMiddleware.NextInput: ~Copyable,
ServerMiddleware.Input == HTTPServerMiddlewareInput<Server.RequestConcludingReader, Server.ResponseConcludingWriter>
ServerMiddleware.Input == HTTPServerMiddlewareInput<Server.RequestContext, Server.RequestConcludingReader, Server.ResponseConcludingWriter>
{
typealias RequestConcludingReader = Server.RequestConcludingReader
typealias ResponseConcludingWriter = Server.ResponseConcludingWriter
Expand Down Expand Up @@ -70,7 +70,7 @@ where
Server.ResponseConcludingWriter: ~Copyable,
Server.ResponseConcludingWriter.Underlying: ~Copyable
{
typealias Input = HTTPServerMiddlewareInput<Server.RequestConcludingReader, Server.ResponseConcludingWriter>
typealias Input = HTTPServerMiddlewareInput<Server.RequestContext, Server.RequestConcludingReader, Server.ResponseConcludingWriter>
typealias NextInput = Input

func intercept<Return: ~Copyable>(
Expand Down
6 changes: 5 additions & 1 deletion Sources/HTTPAPIs/Server/HTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestConcludingReader, ResponseConcludingWriter>: Sendable, ~Copyable, ~Escapable {
public protocol HTTPServer<RequestContext, RequestConcludingReader, ResponseConcludingWriter>: Sendable, ~Copyable, ~Escapable {
associatedtype RequestContext: HTTPServerCapability.RequestContext, ~Copyable

/// 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
Expand Down Expand Up @@ -52,6 +54,8 @@ public protocol HTTPServer<RequestConcludingReader, ResponseConcludingWriter>: S
/// ```
func serve<Handler: HTTPServerRequestHandler>(handler: Handler) async throws
where
Handler.RequestContext: ~Copyable,
Handler.RequestContext == RequestContext,
Handler.RequestReader == RequestConcludingReader,
Handler.RequestReader: ~Copyable,
Handler.ResponseWriter == ResponseConcludingWriter,
Expand Down
66 changes: 66 additions & 0 deletions Sources/HTTPAPIs/Server/HTTPServerCapability+RequestContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//===----------------------------------------------------------------------===//
//
// 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add more docs explaining what a capability is?

///
/// `HTTPServerCapability` groups protocols that represent optional features a server can provide.
/// Each capability protocol defines a set of properties or methods that a server's request context
/// must implement. Libraries and middleware can constrain their generic parameters to require
/// specific capabilities, enabling compile-time verification that a server supports the needed features.
///
/// ## Defining a capability
///
/// ```swift
/// extension HTTPServerCapability {
/// protocol ConnectionInfo: RequestContext {
/// var remoteAddress: String? { get }
/// var localAddress: String? { get }
/// }
/// }
/// ```
///
/// ## Using a capability in middleware or handlers
///
/// ```swift
/// func logConnection<S: HTTPServer>(server: S) async throws
/// where S.RequestContext: HTTPServerCapability.ConnectionInfo {
/// try await server.serve { request, context, body, sender in
/// print("Request from: \(context.remoteAddress ?? "unknown")")
/// // ...
/// }
/// }
/// ```
@available(anyAppleOS 26.0, *)
public enum HTTPServerCapability {
/// A protocol that all server request contexts must conform to.
///
/// `RequestContext` is the base protocol for request contexts provided by HTTP servers.
/// Servers create a context for each incoming request and pass it to the request handler.
/// The context carries metadata about the request that isn't part of the HTTP message itself,
/// such as connection information, routing state, or server-specific data.
///
/// Child protocols (capabilities) extend `RequestContext` with additional properties that
/// a subset of servers provide, allowing libraries to depend on specific capabilities
/// without coupling to a concrete server implementation.
///
/// ## Implementing a custom context
///
/// ```swift
/// struct MyServerContext: HTTPServerCapability.ConnectionInfo {
/// var remoteAddress: String?
/// var localAddress: String?
/// }
/// ```
public protocol RequestContext: ~Copyable, ~Escapable {
}
}
16 changes: 9 additions & 7 deletions Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
/// ```
@available(anyAppleOS 26.0, *)
public struct HTTPServerClosureRequestHandler<
RequestContext: HTTPServerCapability.RequestContext & ~Copyable,
RequestReader: ConcludingAsyncReader & ~Copyable,
ResponseWriter: ConcludingAsyncWriter & ~Copyable,
>: HTTPServerRequestHandler
Expand All @@ -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<ResponseWriter>
) async throws -> Void
Expand All @@ -65,7 +67,7 @@ where
handler:
@Sendable @escaping (
HTTPRequest,
HTTPRequestContext,
consuming RequestContext,
consuming sending RequestReader,
consuming sending HTTPResponseSender<ResponseWriter>
) async throws -> Void
Expand All @@ -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<ResponseWriter>
) async throws {
Expand All @@ -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)
Expand All @@ -130,7 +132,7 @@ where
handler:
@Sendable @escaping (
_ request: HTTPRequest,
_ requestContext: HTTPRequestContext,
_ requestContext: consuming RequestContext,
_ requestBodyAndTrailers: consuming sending RequestConcludingReader,
_ responseSender: consuming sending HTTPResponseSender<ResponseConcludingWriter>
) async throws -> Void
Expand Down
12 changes: 8 additions & 4 deletions Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@
///
/// ```swift
/// struct EchoHandler<
/// Context: HTTPServerCapability.RequestContext,
/// ConcludingRequestReader: ConcludingAsyncReader<RequestReader, HTTPFields?> & ~Copyable,
/// RequestReader: AsyncReader<UInt8, any Error> & ~Copyable,
/// ConcludingResponseWriter: ConcludingAsyncWriter<ResponseWriter, HTTPFields?> & ~Copyable,
/// ResponseWriter: AsyncWriter<UInt8, any Error> & ~Copyable
/// >: HTTPServerRequestHandler {
/// func handle(
/// request: HTTPRequest,
/// requestContext: HTTPRequestContext,
/// requestContext: Context,
/// requestBodyAndTrailers: consuming sending ConcludingRequestReader,
/// responseSender: consuming sending HTTPResponseSender<ConcludingResponseWriter>
/// ) async throws {
Expand All @@ -55,7 +56,10 @@
/// }
/// ```
@available(anyAppleOS 26.0, *)
public protocol HTTPServerRequestHandler<RequestReader, ResponseWriter>: Sendable {
public protocol HTTPServerRequestHandler<RequestContext, RequestReader, ResponseWriter>: Sendable {
/// The type of the request context provided by the server.
associatedtype RequestContext: HTTPServerCapability.RequestContext, ~Copyable

/// 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?
Expand All @@ -76,15 +80,15 @@ public protocol HTTPServerRequestHandler<RequestReader, ResponseWriter>: 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.
///
/// - Throws: Any error encountered during request processing or response generation.
func handle(
request: HTTPRequest,
requestContext: HTTPRequestContext,
requestContext: consuming RequestContext,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is consuming the right ownership model? Could the same Sendable & ~Copyable struct with a Mutex be passed to handle multiple times?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's non copyable, how would it be passed multiple times?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I guess borrow wouldn't work since this is invoked concurrently

requestBodyAndTrailers: consuming sending RequestReader,
responseSender: consuming sending HTTPResponseSender<ResponseWriter>
) async throws
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type should be moved to swift-http-server?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to HTTPServerForTesting

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a PR for depending on the NIOHTTPServer as a submodule.
Before that lands, we'll have to make sure that we've also updated NIOHTTPServer to use this new API.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah probably will land that PR when everything settles to reduce the pain of interlocked revisions

public init() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import NIOPosix
extension NIOHTTPServer {
func serveInsecureHTTP1_1(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>,
handler: some HTTPServerRequestHandler<HTTPRequestContext, RequestConcludingReader, ResponseConcludingWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
) async throws {
let serverChannel = try await self.setupHTTP1_1ServerChannel(
Expand Down Expand Up @@ -71,7 +71,7 @@ extension NIOHTTPServer {

func _serveInsecureHTTP1_1(
serverChannel: NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
handler: some HTTPServerRequestHandler<HTTPRequestContext, RequestConcludingReader, ResponseConcludingWriter>
) async throws {
try await withThrowingDiscardingTaskGroup { group in
try await serverChannel.executeThenClose { inbound in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ extension NIOHTTPServer {
func serveSecureUpgrade(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>,
handler: some HTTPServerRequestHandler<HTTPRequestContext, RequestConcludingReader, ResponseConcludingWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? = nil
Expand Down Expand Up @@ -120,7 +120,7 @@ extension NIOHTTPServer {

func _serveSecureUpgrade(
serverChannel: NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never>,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
handler: some HTTPServerRequestHandler<HTTPRequestContext, RequestConcludingReader, ResponseConcludingWriter>
) async throws {
try await withThrowingDiscardingTaskGroup { group in
try await serverChannel.executeThenClose { inbound in
Expand Down
Loading
Loading