Skip to content

Commit 0761920

Browse files
committed
Add middleware server example
This PR adds an example showcasing how middleware can be used to intercept requests handled by a server. In detail, this PR adds a logging and request handling middleware. The logging middleware is capable of inspecting every single part of the request including the individual chunks read.
1 parent edd0fb5 commit 0761920

13 files changed

Lines changed: 703 additions & 60 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
public import Middleware
2+
3+
public struct ForwardingMiddleware<Input: ~Copyable & ~Escapable>: Middleware {
4+
public init() {}
5+
6+
public func intercept<Return: ~Copyable>(
7+
input: consuming Input,
8+
next: (consuming Input) async throws -> Return
9+
) async throws -> Return {
10+
try await next(input)
11+
}
12+
}
13+
14+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
15+
extension Middleware {
16+
public func forwarding() -> ForwardingMiddleware<Input> {
17+
ForwardingMiddleware()
18+
}
19+
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift HTTP API Proposal open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift HTTP API Proposal project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
public import HTTPAPIs
16+
public import Logging
17+
public import Middleware
18+
19+
/// A middleware that logs HTTP server requests and responses.
20+
///
21+
/// ``HTTPServerLoggingMiddleware`` wraps the request reader and response writer with logging
22+
/// decorators that output information about the HTTP request path, method, response status,
23+
/// and the number of bytes read from the request body and written to the response body.
24+
/// This middleware is useful for debugging and monitoring HTTP traffic.
25+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
26+
public struct HTTPServerLoggingMiddleware<
27+
RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable,
28+
ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable
29+
>: Middleware
30+
where
31+
RequestConcludingAsyncReader: Escapable,
32+
RequestConcludingAsyncReader.Underlying.ReadElement == UInt8,
33+
RequestConcludingAsyncReader.FinalElement == HTTPFields?,
34+
ResponseConcludingAsyncWriter: Escapable,
35+
ResponseConcludingAsyncWriter.Underlying.WriteElement == UInt8,
36+
ResponseConcludingAsyncWriter.FinalElement == HTTPFields?
37+
{
38+
public typealias Input = HTTPServerMiddlewareInput<RequestConcludingAsyncReader, ResponseConcludingAsyncWriter>
39+
public typealias NextInput = HTTPServerMiddlewareInput<
40+
HTTPRequestLoggingConcludingAsyncReader<RequestConcludingAsyncReader>,
41+
HTTPResponseLoggingConcludingAsyncWriter<ResponseConcludingAsyncWriter>
42+
>
43+
44+
let logger: Logger
45+
46+
/// Creates a new logging middleware.
47+
///
48+
/// - Parameters:
49+
/// - requestConcludingAsyncReaderType: The type of the request reader. Defaults to the inferred type.
50+
/// - responseConcludingAsyncWriterType: The type of the response writer. Defaults to the inferred type.
51+
/// - logger: The logger instance to use for logging HTTP events.
52+
public init(
53+
requestConcludingAsyncReaderType: RequestConcludingAsyncReader.Type = RequestConcludingAsyncReader.self,
54+
responseConcludingAsyncWriterType: ResponseConcludingAsyncWriter.Type = ResponseConcludingAsyncWriter.self,
55+
logger: Logger
56+
) {
57+
self.logger = logger
58+
}
59+
60+
public func intercept<Return: ~Copyable>(
61+
input: consuming Input,
62+
next: (consuming NextInput) async throws -> Return
63+
) async throws -> Return {
64+
try await input.withContents { request, context, requestReader, responseSender in
65+
self.logger.info("Received request \(request.path ?? "unknown" ) \(request.method.rawValue)")
66+
defer {
67+
self.logger.info("Finished request \(request.path ?? "unknown" ) \(request.method.rawValue)")
68+
}
69+
let wrappedReader = HTTPRequestLoggingConcludingAsyncReader(
70+
base: requestReader,
71+
logger: self.logger
72+
)
73+
74+
var maybeSender = Optional(responseSender)
75+
let requestResponseBox = HTTPServerMiddlewareInput(
76+
request: request,
77+
requestContext: context,
78+
requestReader: wrappedReader,
79+
responseSender: HTTPResponseSender { [logger] response in
80+
if let sender = maybeSender.take() {
81+
logger.info("Sending response \(response)")
82+
let writer = try await sender.send(response)
83+
return HTTPResponseLoggingConcludingAsyncWriter(
84+
base: writer,
85+
logger: logger
86+
)
87+
} else {
88+
fatalError("Called closure more than once")
89+
}
90+
} sendInformational: { response in
91+
self.logger.info("Sending informational response \(response)")
92+
try await maybeSender?.sendInformational(response)
93+
}
94+
)
95+
return try await next(requestResponseBox)
96+
}
97+
}
98+
}
99+
100+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
101+
extension Middleware {
102+
/// Creates logging middleware for HTTP servers.
103+
///
104+
/// This middleware logs all incoming requests and outgoing responses, including the request
105+
/// path, method, response status, and the number of bytes read and written in the body.
106+
///
107+
/// - Parameter logger: The logger to use for logging requests and responses.
108+
/// - Returns: A middleware that logs HTTP request and response details.
109+
///
110+
/// ## Example
111+
///
112+
/// ```swift
113+
/// @MiddlewareBuilder
114+
/// func buildMiddleware() -> some Middleware<...> {
115+
/// .logging(logger: Logger(label: "HTTPServer"))
116+
/// .requestHandler()
117+
/// }
118+
/// ```
119+
public func logging<RequestReader, ResponseWriter>(
120+
logger: Logger
121+
) -> HTTPServerLoggingMiddleware<RequestReader, ResponseWriter>
122+
where
123+
Input == HTTPServerMiddlewareInput<RequestReader, ResponseWriter>,
124+
RequestReader: ConcludingAsyncReader & ~Copyable & Escapable,
125+
RequestReader.Underlying.ReadElement == UInt8,
126+
RequestReader.FinalElement == HTTPFields?,
127+
ResponseWriter: ConcludingAsyncWriter & ~Copyable & Escapable,
128+
ResponseWriter.Underlying.WriteElement == UInt8,
129+
ResponseWriter.FinalElement == HTTPFields?
130+
{
131+
HTTPServerLoggingMiddleware(logger: logger)
132+
}
133+
}
134+
135+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
136+
public struct HTTPRequestLoggingConcludingAsyncReader<
137+
Base: ConcludingAsyncReader & ~Copyable
138+
>: ConcludingAsyncReader, ~Copyable
139+
where
140+
Base.Underlying.ReadElement == UInt8,
141+
Base.FinalElement == HTTPFields?
142+
{
143+
public typealias Underlying = RequestBodyAsyncReader
144+
public typealias FinalElement = HTTPFields?
145+
146+
public struct RequestBodyAsyncReader: AsyncReader, ~Copyable, ~Escapable {
147+
public typealias ReadElement = UInt8
148+
public typealias ReadFailure = Base.Underlying.ReadFailure
149+
150+
private var underlying: Base.Underlying
151+
private let logger: Logger
152+
153+
@_lifetime(copy underlying)
154+
init(underlying: consuming Base.Underlying, logger: Logger) {
155+
self.underlying = underlying
156+
self.logger = logger
157+
}
158+
159+
@_lifetime(self: copy self)
160+
public mutating func read<Return, Failure>(
161+
maximumCount: Int?,
162+
body: (consuming Span<UInt8>) async throws(Failure) -> Return
163+
) async throws(EitherError<Base.Underlying.ReadFailure, Failure>) -> Return {
164+
return try await self.underlying.read(
165+
maximumCount: maximumCount
166+
) { (span: Span<UInt8>) async throws(Failure) -> Return in
167+
logger.info("Received next chunk \(span.count)")
168+
return try await body(span)
169+
}
170+
}
171+
}
172+
173+
private var base: Base
174+
private let logger: Logger
175+
176+
init(base: consuming Base, logger: Logger) {
177+
self.base = base
178+
self.logger = logger
179+
}
180+
181+
public consuming func consumeAndConclude<Return, Failure>(
182+
body: (consuming sending RequestBodyAsyncReader) async throws(Failure) -> Return
183+
) async throws(Failure) -> (Return, HTTPTypes.HTTPFields?) {
184+
let (result, trailers) = try await self.base.consumeAndConclude { [logger] reader async throws(Failure) -> Return in
185+
let wrappedReader = RequestBodyAsyncReader(
186+
underlying: reader,
187+
logger: logger
188+
)
189+
return try await body(wrappedReader)
190+
}
191+
192+
if let trailers {
193+
self.logger.info("Received request trailers \(trailers)")
194+
} else {
195+
self.logger.info("Received no request trailers")
196+
}
197+
198+
return (result, trailers)
199+
}
200+
}
201+
202+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
203+
public struct HTTPResponseLoggingConcludingAsyncWriter<
204+
Base: ConcludingAsyncWriter & ~Copyable
205+
>: ConcludingAsyncWriter, ~Copyable
206+
where
207+
Base.Underlying.WriteElement == UInt8,
208+
Base.FinalElement == HTTPFields?
209+
{
210+
public typealias Underlying = ResponseBodyAsyncWriter
211+
public typealias FinalElement = HTTPFields?
212+
213+
public struct ResponseBodyAsyncWriter: AsyncWriter, ~Copyable, ~Escapable {
214+
public typealias WriteElement = UInt8
215+
public typealias WriteFailure = Base.Underlying.WriteFailure
216+
217+
private var underlying: Base.Underlying
218+
private let logger: Logger
219+
220+
@_lifetime(copy underlying)
221+
init(underlying: consuming Base.Underlying, logger: Logger) {
222+
self.underlying = underlying
223+
self.logger = logger
224+
}
225+
226+
@_lifetime(self: copy self)
227+
public mutating func write<Result, Failure>(
228+
_ body: (inout OutputSpan<UInt8>) async throws(Failure) -> Result
229+
) async throws(EitherError<Base.Underlying.WriteFailure, Failure>) -> Result {
230+
return try await self.underlying.write { (outputSpan: inout OutputSpan<UInt8>) async throws(Failure) -> Result in
231+
defer {
232+
self.logger.info("Wrote response bytes \(outputSpan.count)")
233+
}
234+
return try await body(&outputSpan)
235+
}
236+
}
237+
}
238+
239+
private var base: Base
240+
private let logger: Logger
241+
242+
init(base: consuming Base, logger: Logger) {
243+
self.base = base
244+
self.logger = logger
245+
}
246+
247+
public consuming func produceAndConclude<Return>(
248+
body: (consuming sending ResponseBodyAsyncWriter) async throws -> (Return, HTTPFields?)
249+
) async throws -> Return {
250+
let logger = self.logger
251+
return try await self.base.produceAndConclude { writer in
252+
let wrappedAsyncWriter = ResponseBodyAsyncWriter(underlying: writer, logger: logger)
253+
let (result, trailers) = try await body(wrappedAsyncWriter)
254+
255+
if let trailers {
256+
logger.info("Wrote response trailers \(trailers)")
257+
} else {
258+
logger.info("Wrote no response trailers")
259+
}
260+
return (result, trailers)
261+
}
262+
}
263+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift HTTP API Proposal open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift HTTP API Proposal project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift HTTP API Proposal project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
public import HTTPAPIs
16+
17+
/// A struct that encapsulates all parameters passed to HTTP server request handlers.
18+
///
19+
/// ``HTTPServerMiddlewareInput`` serves as a container for the request, request context,
20+
/// request body reader, and response sender. This boxing is necessary because some of these
21+
/// parameters are `~Copyable` types that cannot be stored in tuples, and it provides a
22+
/// convenient way to pass all request-handling components through the middleware chain.
23+
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
24+
public struct HTTPServerMiddlewareInput<
25+
RequestReader: ConcludingAsyncReader & ~Copyable,
26+
ResponseWriter: ConcludingAsyncWriter & ~Copyable
27+
>: ~Copyable {
28+
private let request: HTTPRequest
29+
private let requestContext: HTTPRequestContext
30+
private let requestReader: RequestReader
31+
private let responseSender: HTTPResponseSender<ResponseWriter>
32+
33+
/// Creates a new HTTP server middleware input container.
34+
///
35+
/// - Parameters:
36+
/// - request: The HTTP request headers and metadata.
37+
/// - requestContext: Additional context information for the request.
38+
/// - requestReader: A reader for accessing the request body data and trailing headers.
39+
/// - responseSender: A sender for transmitting the HTTP response and response body.
40+
public init(
41+
request: HTTPRequest,
42+
requestContext: HTTPRequestContext,
43+
requestReader: consuming RequestReader,
44+
responseSender: consuming HTTPResponseSender<ResponseWriter>
45+
) {
46+
self.request = request
47+
self.requestContext = requestContext
48+
self.requestReader = requestReader
49+
self.responseSender = responseSender
50+
}
51+
52+
/// Provides scoped access to the contents of this input container.
53+
///
54+
/// This method exposes all the encapsulated request components to a closure, allowing
55+
/// middleware to access and process them. The closure receives the request, request context,
56+
/// request reader, and response sender as separate parameters.
57+
///
58+
/// - Parameter handler: A closure that processes the request components.
59+
///
60+
/// - Returns: The value returned by the handler closure.
61+
///
62+
/// - Throws: Any error thrown by the handler closure.
63+
public consuming func withContents<Return: ~Copyable>(
64+
_ handler:
65+
(
66+
HTTPRequest,
67+
HTTPRequestContext,
68+
consuming RequestReader,
69+
consuming HTTPResponseSender<ResponseWriter>
70+
) async throws -> Return
71+
) async throws -> Return {
72+
try await handler(
73+
self.request,
74+
self.requestContext,
75+
self.requestReader,
76+
self.responseSender
77+
)
78+
}
79+
}
80+
81+
@available(*, unavailable)
82+
extension HTTPServerMiddlewareInput: Sendable {}

0 commit comments

Comments
 (0)