Skip to content

Commit b5cacb9

Browse files
authored
Add a RequestContext to the request handler (#26)
1 parent ce5d41b commit b5cacb9

11 files changed

+110
-62
lines changed

Sources/Example/Example.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ struct Example {
2222

2323
// Using the new extension method that doesn't require type hints
2424
let privateKey = P256.Signing.PrivateKey()
25-
let server = NIOHTTPServer<HTTPServerClosureRequestHandler<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter>>(
25+
let server = NIOHTTPServer<HTTPServerClosureRequestHandler<
26+
HTTPRequestConcludingAsyncReader,
27+
HTTPRequestConcludingAsyncReader.Underlying,
28+
HTTPResponseConcludingAsyncWriter,
29+
HTTPResponseConcludingAsyncWriter.Underlying
30+
>>(
2631
logger: logger,
2732
configuration: .init(
2833
bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345),
@@ -45,7 +50,7 @@ struct Example {
4550
)
4651
)
4752
)
48-
try await server.serve { request, requestBodyAndTrailers, responseSender in
53+
try await server.serve { request, requestContext, requestBodyAndTrailers, responseSender in
4954
let writer = try await responseSender.send(HTTPResponse(status: .ok))
5055
try await writer.writeAndConclude(element: "Well, hello!".utf8.span, finalElement: nil)
5156
}
@@ -54,16 +59,20 @@ struct Example {
5459

5560
// MARK: - Server Extensions
5661

57-
// This has to be commented out because of the compiler bug above. Workaround doesn't apply here.
58-
5962
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
60-
extension NIOHTTPServer where RequestHandler == HTTPServerClosureRequestHandler<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter> {
63+
extension NIOHTTPServer
64+
where RequestHandler == HTTPServerClosureRequestHandler<
65+
HTTPRequestConcludingAsyncReader,
66+
HTTPRequestConcludingAsyncReader.Underlying,
67+
HTTPResponseConcludingAsyncWriter,
68+
HTTPResponseConcludingAsyncWriter.Underlying
69+
> {
6170
/// Serve HTTP requests using a middleware chain built with the provided builder
6271
/// This method handles the type inference for HTTP middleware components
6372
func serve(
6473
@MiddlewareChainBuilder
6574
withMiddleware middlewareBuilder: () -> some Middleware<
66-
RequestResponseMiddlewareBox<
75+
RequestResponseMiddlewareBox<
6776
HTTPRequestConcludingAsyncReader,
6877
HTTPResponseConcludingAsyncWriter
6978
>,
@@ -72,9 +81,10 @@ extension NIOHTTPServer where RequestHandler == HTTPServerClosureRequestHandler<
7281
) async throws {
7382
let chain = middlewareBuilder()
7483

75-
try await self.serve { request, reader, responseSender in
84+
try await self.serve { request, requestContext, reader, responseSender in
7685
try await chain.intercept(input: RequestResponseMiddlewareBox(
7786
request: request,
87+
requestContext: requestContext,
7888
requestReader: reader,
7989
responseSender: responseSender
8090
)) { _ in }

Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ where
3434
input: consuming Input,
3535
next: (consuming NextInput) async throws -> Void
3636
) async throws {
37-
try await input.withContents { request, requestReader, responseSender in
37+
try await input.withContents { request, context, requestReader, responseSender in
3838
self.logger.info("Received request \(request.path ?? "unknown" ) \(request.method.rawValue)")
3939
defer {
4040
self.logger.info("Finished request \(request.path ?? "unknown" ) \(request.method.rawValue)")
@@ -47,6 +47,7 @@ where
4747
var maybeSender = Optional(responseSender)
4848
let requestResponseBox = RequestResponseMiddlewareBox(
4949
request: request,
50+
requestContext: context,
5051
requestReader: wrappedReader,
5152
responseSender: HTTPResponseSender { [logger] response in
5253
if let sender = maybeSender.take() {

Sources/Example/Middlewares/RouteHandlerMiddleware.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ where
2020
input: consuming Input,
2121
next: (consuming NextInput) async throws -> Void
2222
) async throws {
23-
try await input.withContents { request, requestReader, responseSender in
23+
try await input.withContents { request, _, requestReader, responseSender in
2424
var maybeReader = Optional(requestReader)
2525
try await responseSender.send(HTTPResponse(status: .accepted))
2626
.produceAndConclude { responseBodyAsyncWriter in
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/// A context object that carries additional information about an HTTP request.
2+
///
3+
/// `HTTPRequestContext` provides a way to pass metadata through the HTTP request pipeline.
4+
public struct HTTPRequestContext: Sendable {}

Sources/HTTPServer/HTTPResponseSender.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ public import HTTPTypes
33
/// This type ensures that a single non-informational (1xx) `HTTPResponse` is sent back to the client when handling a request.
44
///
55
/// The user will get a ``HTTPResponseSender`` as part of
6-
/// ``HTTPServerRequestHandler/handle(request:requestBodyAndTrailers:responseSender:)``, and they
6+
/// ``HTTPServerRequestHandler/handle(request:requestContext:requestBodyAndTrailers:responseSender:)``, and they
77
/// will only be allowed to call ``send(_:)`` once before the sender is consumed and cannot be referenced again.
88
/// ``sendInformational(_:)`` may be called zero or more times.
99
///

Sources/HTTPServer/HTTPServerClosureRequestHandler.swift

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,33 @@ public import HTTPTypes
88
///
99
/// - Example:
1010
/// ```swift
11-
/// let echoHandler = HTTPServerClosureRequestHandler { request, bodyReader, sendResponse in
11+
/// let echoHandler = HTTPServerClosureRequestHandler { request, context, bodyReader, responseSender in
1212
/// // Read the entire request body
1313
/// let (bodyData, _) = try await bodyReader.consumeAndConclude { reader in
1414
/// // ... body reading code ...
1515
/// }
1616
///
1717
/// // Create and send response
1818
/// var response = HTTPResponse(status: .ok)
19-
/// let responseWriter = try await sendResponse(response)
19+
/// let responseWriter = try await responseSender.send(response)
2020
/// try await responseWriter.produceAndConclude { writer in
2121
/// try await writer.write(bodyData.span)
2222
/// return ((), nil)
2323
/// }
2424
/// }
2525
/// ```
2626
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
27-
public struct HTTPServerClosureRequestHandler<ConcludingRequestReader: ~Copyable, ConcludingResponseWriter: ~Copyable>: HTTPServerRequestHandler {
27+
public struct HTTPServerClosureRequestHandler<
28+
ConcludingRequestReader: ConcludingAsyncReader<RequestReader, HTTPFields?> & ~Copyable,
29+
RequestReader: AsyncReader<Span<UInt8>, any Error> & ~Copyable,
30+
ConcludingResponseWriter: ConcludingAsyncWriter<RequestWriter, HTTPFields?> & ~Copyable,
31+
RequestWriter: AsyncWriter<Span<UInt8>, any Error> & ~Copyable
32+
>: HTTPServerRequestHandler {
2833
/// The underlying closure that handles HTTP requests
2934
private let _handler:
3035
nonisolated(nonsending) @Sendable (
3136
HTTPRequest,
37+
HTTPRequestContext,
3238
consuming sending HTTPRequestConcludingAsyncReader,
3339
consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
3440
) async throws -> Void
@@ -40,6 +46,7 @@ public struct HTTPServerClosureRequestHandler<ConcludingRequestReader: ~Copyable
4046
public init(
4147
handler: nonisolated(nonsending) @Sendable @escaping (
4248
HTTPRequest,
49+
HTTPRequestContext,
4350
consuming sending HTTPRequestConcludingAsyncReader,
4451
consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
4552
) async throws -> Void
@@ -53,13 +60,58 @@ public struct HTTPServerClosureRequestHandler<ConcludingRequestReader: ~Copyable
5360
///
5461
/// - Parameters:
5562
/// - request: The HTTP request headers and metadata.
63+
/// - requestContext: A ``HTTPRequestContext``.
5664
/// - requestBodyAndTrailers: A reader for accessing the request body data and trailing headers.
5765
/// - responseSender: An ``HTTPResponseSender`` to send the HTTP response.
5866
public func handle(
5967
request: HTTPRequest,
68+
requestContext: HTTPRequestContext,
6069
requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader,
6170
responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
6271
) async throws {
63-
try await self._handler(request, requestBodyAndTrailers, responseSender)
72+
try await self._handler(request, requestContext, requestBodyAndTrailers, responseSender)
73+
}
74+
}
75+
76+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
77+
extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHandler<
78+
HTTPRequestConcludingAsyncReader,
79+
HTTPRequestConcludingAsyncReader.Underlying,
80+
HTTPResponseConcludingAsyncWriter,
81+
HTTPResponseConcludingAsyncWriter.Underlying
82+
> {
83+
/// Starts an HTTP server with a closure-based request handler.
84+
///
85+
/// This method provides a convenient way to start an HTTP server using a closure to handle incoming requests.
86+
///
87+
/// - Parameters:
88+
/// - handler: An async closure that processes HTTP requests. The closure receives:
89+
/// - `HTTPRequest`: The incoming HTTP request with headers and metadata.
90+
/// - ``HTTPRequestContext``: The request's context.
91+
/// - ``HTTPRequestConcludingAsyncReader``: An async reader for consuming the request body and trailers.
92+
/// - ``HTTPResponseSender``: A non-copyable wrapper for a function that accepts an `HTTPResponse` and provides access to an ``HTTPResponseConcludingAsyncWriter``.
93+
///
94+
/// ## Example
95+
///
96+
/// ```swift
97+
/// try await server.serve { request, bodyReader, responseSender in
98+
/// // Process the request
99+
/// let response = HTTPResponse(status: .ok)
100+
/// let writer = try await responseSender.send(response)
101+
/// try await writer.produceAndConclude { writer in
102+
/// try await writer.write("Hello, World!".utf8)
103+
/// return ((), nil)
104+
/// }
105+
/// }
106+
/// ```
107+
public func serve(
108+
handler: @Sendable @escaping (
109+
_ request: HTTPRequest,
110+
_ requestContext: HTTPRequestContext,
111+
_ requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader,
112+
_ responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
113+
) async throws -> Void
114+
) async throws {
115+
try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler))
64116
}
65117
}

Sources/HTTPServer/HTTPServerProtocol.swift

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ public import HTTPTypes
33
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
44
/// A generic HTTP server protocol that can handle incoming HTTP requests.
55
public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable {
6-
// TODO: write down in the proposal why we can't make the serve method generic over the handler (closure-based APIs can't
7-
// be implemented)
6+
// TODO: write down in the proposal we can't make the serve method generic over the handler
7+
// because otherwise, closure-based APIs can't be implemented.
88

99
/// The ``HTTPServerRequestHandler`` to use when handling requests.
1010
associatedtype RequestHandler: HTTPServerRequestHandler
@@ -41,39 +41,3 @@ public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable {
4141
/// ```
4242
func serve(handler: RequestHandler) async throws
4343
}
44-
45-
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
46-
extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHandler<HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter> {
47-
/// Starts an HTTP server with a closure-based request handler.
48-
///
49-
/// This method provides a convenient way to start an HTTP server using a closure to handle incoming requests.
50-
///
51-
/// - Parameters:
52-
/// - handler: An async closure that processes HTTP requests. The closure receives:
53-
/// - `HTTPRequest`: The incoming HTTP request with headers and metadata
54-
/// - ``HTTPRequestConcludingAsyncReader``: An async reader for consuming the request body and trailers
55-
/// - ``HTTPResponseSender``: A non-copyable wrapper for a function that accepts an `HTTPResponse` and provides access to an ``HTTPResponseConcludingAsyncWriter``
56-
///
57-
/// ## Example
58-
///
59-
/// ```swift
60-
/// try await server.serve { request, bodyReader, sendResponse in
61-
/// // Process the request
62-
/// let response = HTTPResponse(status: .ok)
63-
/// let writer = try await sendResponse(response)
64-
/// try await writer.produceAndConclude { writer in
65-
/// try await writer.write("Hello, World!".utf8)
66-
/// return ((), nil)
67-
/// }
68-
/// }
69-
/// ```
70-
public func serve(
71-
handler: nonisolated(nonsending) @Sendable @escaping (
72-
_ request: HTTPRequest,
73-
_ requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader,
74-
_ responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
75-
) async throws -> Void
76-
) async throws {
77-
try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler))
78-
}
79-
}

Sources/HTTPServer/HTTPServerRequestHandler.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ public import HTTPTypes
1616
///
1717
/// ```swift
1818
/// struct EchoHandler: HTTPServerRequestHandler {
19-
/// func handle(
19+
/// func handle(
2020
/// request: HTTPRequest,
21-
/// requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader,
22-
/// responseSender: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
23-
/// ) async throws {
21+
/// requestContext: HTTPRequestContext,
22+
/// requestConcludingAsyncReader: consuming sending HTTPRequestConcludingAsyncReader,
23+
/// responseSender: consuming sending HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
24+
/// ) async throws {
2425
/// // Read the entire request body
2526
/// let (bodyData, trailers) = try await requestConcludingAsyncReader.consumeAndConclude { reader in
2627
/// var reader = reader
@@ -81,12 +82,13 @@ public protocol HTTPServerRequestHandler: Sendable {
8182
/// 1. Examine the request headers in the `request` parameter
8283
/// 2. Read the request body data from the ``RequestConcludingAsyncReader`` as needed
8384
/// 3. Process the request and prepare a response
84-
/// 4. Optionally call ``HTTPResponseSender/sendInformationalResponse(_:)`` as needed
85-
/// 4. Call the ``HTTPResponseSender/sendResponse(_:)`` with an appropriate HTTP response
85+
/// 4. Optionally call ``HTTPResponseSender/sendInformational(_:)`` as needed
86+
/// 4. Call the ``HTTPResponseSender/send(_:)`` with an appropriate HTTP response
8687
/// 5. Write the response body data to the returned ``HTTPResponseConcludingAsyncWriter``
8788
///
8889
/// - Parameters:
8990
/// - request: The HTTP request headers and metadata.
91+
/// - requestContext: A ``HTTPRequestContext``.
9092
/// - requestBodyAndTrailers: A reader for accessing the request body data and trailing headers.
9193
/// This follows the `ConcludingAsyncReader` pattern, allowing for incremental reading of request body data
9294
/// and concluding with any trailer fields sent at the end of the request.
@@ -95,8 +97,8 @@ public protocol HTTPServerRequestHandler: Sendable {
9597
///
9698
/// - Throws: Any error encountered during request processing or response generation.
9799
func handle(
98-
// TODO: add request context parameter
99100
request: HTTPRequest,
101+
requestContext: HTTPRequestContext,
100102
requestBodyAndTrailers: consuming sending ConcludingRequestReader,
101103
responseSender: consuming sending HTTPResponseSender<ConcludingResponseWriter>
102104
) async throws

Sources/HTTPServer/NIOHTTPServer.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader
424424
do {
425425
try await handler.handle(
426426
request: httpRequest,
427+
requestContext: HTTPRequestContext(),
427428
requestBodyAndTrailers: HTTPRequestConcludingAsyncReader(
428429
iterator: iterator,
429430
readerState: readerState

Sources/HTTPServer/RequestResponseMiddlewareBox.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public struct RequestResponseMiddlewareBox<
88
ResponseWriter: ConcludingAsyncWriter & ~Copyable
99
>: ~Copyable {
1010
private let request: HTTPRequest
11+
private let requestContext: HTTPRequestContext
1112
private let requestReader: RequestReader
1213
private let responseSender: HTTPResponseSender<ResponseWriter>
1314

@@ -18,10 +19,12 @@ public struct RequestResponseMiddlewareBox<
1819
/// - responseSender: The ``HTTPResponseSender``.
1920
public init(
2021
request: HTTPRequest,
22+
requestContext: HTTPRequestContext,
2123
requestReader: consuming RequestReader,
2224
responseSender: consuming HTTPResponseSender<ResponseWriter>
2325
) {
2426
self.request = request
27+
self.requestContext = requestContext
2528
self.requestReader = requestReader
2629
self.responseSender = responseSender
2730
}
@@ -32,11 +35,17 @@ public struct RequestResponseMiddlewareBox<
3235
public consuming func withContents<T>(
3336
_ handler: nonisolated(nonsending) (
3437
HTTPRequest,
38+
HTTPRequestContext,
3539
consuming RequestReader,
3640
consuming HTTPResponseSender<ResponseWriter>
3741
) async throws -> T
3842
) async throws -> T {
39-
try await handler(self.request, self.requestReader, self.responseSender)
43+
try await handler(
44+
self.request,
45+
self.requestContext,
46+
self.requestReader,
47+
self.responseSender
48+
)
4049
}
4150
}
4251

0 commit comments

Comments
 (0)