Skip to content

Commit a714c49

Browse files
authored
Make all serve parameters consuming (#11)
This PR adds the necessary annotations to make all reader and writer types `~Copyable` so that we can enforce call-once behaviour on the reader and writers. It also exposes some additional state from the reader and writer to the server so that we can fail accordingly.
1 parent 884deb2 commit a714c49

19 files changed

+397
-224
lines changed

Sources/Example/Example.swift

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -43,58 +43,52 @@ struct Example {
4343
],
4444
privateKey: Certificate.PrivateKey(privateKey)
4545
)
46-
),
47-
withMiddleware: {
48-
HTTPRequestLoggingMiddleware<
49-
HTTPRequestConcludingAsyncReader,
50-
HTTPResponseConcludingAsyncWriter
51-
>(logger: logger)
52-
TracingMiddleware<
53-
(
54-
HTTPRequest,
55-
HTTPRequestLoggingConcludingAsyncReader<HTTPRequestConcludingAsyncReader>,
56-
(
57-
HTTPResponse
58-
) async throws -> HTTPResponseLoggingConcludingAsyncWriter<HTTPResponseConcludingAsyncWriter>
59-
)
60-
>()
61-
RouteHandlerMiddleware<
62-
HTTPRequestLoggingConcludingAsyncReader<HTTPRequestConcludingAsyncReader>,
63-
HTTPResponseLoggingConcludingAsyncWriter<HTTPResponseConcludingAsyncWriter>
64-
>()
65-
}
66-
)
46+
), handler: handler(request:requestConcludingAsyncReader:responseSender:))
47+
}
48+
49+
// This is a workaround for a current bug with the compiler.
50+
@Sendable
51+
nonisolated(nonsending) private static func handler(
52+
request: HTTPRequest,
53+
requestConcludingAsyncReader: consuming HTTPRequestConcludingAsyncReader,
54+
responseSender: consuming HTTPResponseSender<HTTPResponseConcludingAsyncWriter>
55+
) async throws {
56+
let writer = try await responseSender.sendResponse(HTTPResponse(status: .ok))
57+
try await writer.writeAndConclude(element: "Well, hello!".utf8.span, finalElement: nil)
6758
}
6859
}
6960

7061
// MARK: - Server Extensions
7162

72-
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
73-
extension Server {
74-
/// Serve HTTP requests using a middleware chain built with the provided builder
75-
/// This method handles the type inference for HTTP middleware components
76-
static func serve(
77-
logger: Logger,
78-
configuration: HTTPServerConfiguration,
79-
@MiddlewareChainBuilder
80-
withMiddleware middlewareBuilder: () -> some Middleware<
81-
(
82-
HTTPRequest,
83-
HTTPRequestConcludingAsyncReader,
84-
(
85-
HTTPResponse
86-
) async throws -> HTTPResponseConcludingAsyncWriter
87-
),
88-
Never
89-
> & Sendable
90-
) async throws where RequestHandler == HTTPServerClosureRequestHandler {
91-
let chain = middlewareBuilder()
63+
// This has to be commented out because of the compiler bug above. Workaround doesn't apply here.
9264

93-
try await serve(
94-
logger: logger,
95-
configuration: configuration
96-
) { request, requestReader, sendResponse in
97-
try await chain.intercept(input: (request, requestReader, sendResponse)) { _ in }
98-
}
99-
}
100-
}
65+
//@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
66+
//extension Server {
67+
// /// Serve HTTP requests using a middleware chain built with the provided builder
68+
// /// This method handles the type inference for HTTP middleware components
69+
// static func serve(
70+
// logger: Logger,
71+
// configuration: HTTPServerConfiguration,
72+
// @MiddlewareChainBuilder
73+
// withMiddleware middlewareBuilder: () -> some Middleware<
74+
// RequestResponseContext<
75+
// HTTPRequestConcludingAsyncReader,
76+
// HTTPResponseConcludingAsyncWriter
77+
// >,
78+
// Never
79+
// > & Sendable
80+
// ) async throws where RequestHandler == HTTPServerClosureRequestHandler {
81+
// let chain = middlewareBuilder()
82+
//
83+
// try await serve(
84+
// logger: logger,
85+
// configuration: configuration
86+
// ) { request, reader, responseSender in
87+
// try await chain.intercept(input: RequestResponseContext(
88+
// request: request,
89+
// requestReader: reader,
90+
// responseSender: responseSender
91+
// )) { _ in }
92+
// }
93+
// }
94+
//}

Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,90 +5,92 @@ import Middleware
55

66
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
77
struct HTTPRequestLoggingMiddleware<
8-
RequestConludingAsyncReader: ConcludingAsyncReader,
8+
RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable,
99
ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable
1010
>: Middleware
1111
where
12-
RequestConludingAsyncReader.Underlying: AsyncReader<Span<UInt8>, any Error>,
13-
RequestConludingAsyncReader.FinalElement == HTTPFields?,
14-
ResponseConcludingAsyncWriter.Underlying: AsyncWriter<Span<UInt8>, any Error>,
12+
RequestConcludingAsyncReader.Underlying.ReadElement == Span<UInt8>,
13+
RequestConcludingAsyncReader.FinalElement == HTTPFields?,
14+
ResponseConcludingAsyncWriter.Underlying.WriteElement == Span<UInt8>,
1515
ResponseConcludingAsyncWriter.FinalElement == HTTPFields?
1616
{
17-
typealias Input = (
18-
HTTPRequest, RequestConludingAsyncReader, (HTTPResponse) async throws -> ResponseConcludingAsyncWriter
19-
)
20-
typealias NextInput = (
21-
HTTPRequest,
22-
HTTPRequestLoggingConcludingAsyncReader<RequestConludingAsyncReader>,
23-
(
24-
HTTPResponse
25-
) async throws -> HTTPResponseLoggingConcludingAsyncWriter<ResponseConcludingAsyncWriter>
26-
)
17+
typealias Input = RequestResponseMiddlewareBox<RequestConcludingAsyncReader, ResponseConcludingAsyncWriter>
18+
typealias NextInput = RequestResponseMiddlewareBox<
19+
HTTPRequestLoggingConcludingAsyncReader<RequestConcludingAsyncReader>,
20+
HTTPResponseLoggingConcludingAsyncWriter<ResponseConcludingAsyncWriter>
21+
>
2722

2823
let logger: Logger
2924

3025
init(
31-
requestConludingAsyncReaderType: RequestConludingAsyncReader.Type = RequestConludingAsyncReader.self,
26+
requestConcludingAsyncReaderType: RequestConcludingAsyncReader.Type = RequestConcludingAsyncReader.self,
3227
responseConcludingAsyncWriterType: ResponseConcludingAsyncWriter.Type = ResponseConcludingAsyncWriter.self,
3328
logger: Logger
3429
) {
3530
self.logger = logger
3631
}
3732

3833
func intercept(
39-
input: Input,
40-
next: (NextInput) async throws -> Void
34+
input: consuming Input,
35+
next: (consuming NextInput) async throws -> Void
4136
) async throws {
42-
let request = input.0
43-
let requestAsyncReader = input.1
44-
let respond = input.2
45-
self.logger.info("Received request \(request.path ?? "unknown" ) \(request.method.rawValue)")
46-
defer {
47-
self.logger.info("Finished request \(request.path ?? "unknown" ) \(request.method.rawValue)")
48-
}
49-
let wrappedReader = HTTPRequestLoggingConcludingAsyncReader(
50-
base: requestAsyncReader,
51-
logger: self.logger
52-
)
53-
try await next(
54-
(request, wrappedReader, { httpResponse in
55-
let writer = try await respond(httpResponse)
56-
return HTTPResponseLoggingConcludingAsyncWriter(
57-
base: writer,
58-
logger: self.logger
59-
)
37+
try await input.withContents { request, requestReader, responseSender in
38+
self.logger.info("Received request \(request.path ?? "unknown" ) \(request.method.rawValue)")
39+
defer {
40+
self.logger.info("Finished request \(request.path ?? "unknown" ) \(request.method.rawValue)")
41+
}
42+
let wrappedReader = HTTPRequestLoggingConcludingAsyncReader(
43+
base: requestReader,
44+
logger: self.logger
45+
)
46+
47+
var maybeSender = Optional(responseSender)
48+
let requestResponseBox = RequestResponseMiddlewareBox(
49+
request: request,
50+
requestReader: wrappedReader,
51+
responseSender: HTTPResponseSender { [logger] response in
52+
if let sender = maybeSender.take() {
53+
let writer = try await sender.sendResponse(response)
54+
return HTTPResponseLoggingConcludingAsyncWriter(
55+
base: writer,
56+
logger: logger
57+
)
58+
} else {
59+
fatalError("Called closure more than once")
60+
}
6061
}
6162
)
62-
)
63+
try await next(requestResponseBox)
64+
}
6365
}
6466
}
6567

6668
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
6769
struct HTTPRequestLoggingConcludingAsyncReader<
68-
Base: ConcludingAsyncReader
69-
>: ConcludingAsyncReader
70+
Base: ConcludingAsyncReader & ~Copyable
71+
>: ConcludingAsyncReader, ~Copyable
7072
where
71-
Base.Underlying: AsyncReader<Span<UInt8>, any Error>,
73+
Base.Underlying.ReadElement == Span<UInt8>,
7274
Base.FinalElement == HTTPFields?
7375
{
7476
typealias Underlying = RequestBodyAsyncReader
7577
typealias FinalElement = HTTPFields?
7678

77-
struct RequestBodyAsyncReader: AsyncReader {
79+
struct RequestBodyAsyncReader: AsyncReader, ~Copyable {
7880
typealias ReadElement = Span<UInt8>
7981
typealias ReadFailure = any Error
8082

8183
private var underlying: Base.Underlying
8284
private let logger: Logger
8385

84-
init(underlying: Base.Underlying, logger: Logger) {
86+
init(underlying: consuming Base.Underlying, logger: Logger) {
8587
self.underlying = underlying
8688
self.logger = logger
8789
}
8890

8991
mutating func read<Return>(
9092
body: (consuming Span<UInt8>?) async throws -> Return
91-
) async throws(any Error) -> Return {
93+
) async throws -> Return {
9294
let logger = self.logger
9395
return try await self.underlying.read { span in
9496
logger.info("Received next chunk \(span?.count ?? 0)")
@@ -100,27 +102,28 @@ where
100102
private var base: Base
101103
private let logger: Logger
102104

103-
init(base: Base, logger: Logger) {
105+
init(base: consuming Base, logger: Logger) {
104106
self.base = base
105107
self.logger = logger
106108
}
107109

108-
func consumeAndConclude<Return>(
109-
body: (inout RequestBodyAsyncReader) async throws -> Return
110-
) async throws -> (Return, HTTPFields?) {
111-
let (result, trailers) = try await self.base.consumeAndConclude { bodyAsyncReader in
112-
var wrappedReader = RequestBodyAsyncReader(
113-
underlying: bodyAsyncReader,
114-
logger: self.logger
110+
consuming func consumeAndConclude<Return>(
111+
body: (consuming Underlying) async throws -> Return
112+
) async throws -> (Return, FinalElement) {
113+
let (result, trailers) = try await self.base.consumeAndConclude { [logger] reader in
114+
let wrappedReader = RequestBodyAsyncReader(
115+
underlying: reader,
116+
logger: logger
115117
)
116-
return try await body(&wrappedReader)
118+
return try await body(wrappedReader)
117119
}
118120

119121
if let trailers {
120122
self.logger.info("Received request trailers \(trailers)")
121123
} else {
122124
self.logger.info("Received no request trailers")
123125
}
126+
124127
return (result, trailers)
125128
}
126129
}
@@ -130,7 +133,7 @@ struct HTTPResponseLoggingConcludingAsyncWriter<
130133
Base: ConcludingAsyncWriter & ~Copyable
131134
>: ConcludingAsyncWriter, ~Copyable
132135
where
133-
Base.Underlying: AsyncWriter<Span<UInt8>, any Error>,
136+
Base.Underlying.WriteElement == Span<UInt8>,
134137
Base.FinalElement == HTTPFields?
135138
{
136139
typealias Underlying = ResponseBodyAsyncWriter

Sources/Example/Middlewares/RouteHandlerMiddleware.swift

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Middleware
44

55
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
66
struct RouteHandlerMiddleware<
7-
RequestConcludingAsyncReader: ConcludingAsyncReader & Copyable,
7+
RequestConcludingAsyncReader: ConcludingAsyncReader & ~Copyable,
88
ResponseConcludingAsyncWriter: ConcludingAsyncWriter & ~Copyable,
99
>: Middleware, Sendable
1010
where
@@ -13,38 +13,37 @@ where
1313
ResponseConcludingAsyncWriter.Underlying: AsyncWriter<Span<UInt8>, any Error>,
1414
ResponseConcludingAsyncWriter.FinalElement == HTTPFields?
1515
{
16-
typealias Input = (
17-
HTTPRequest,
18-
RequestConcludingAsyncReader,
19-
(HTTPResponse) async throws -> ResponseConcludingAsyncWriter
20-
)
16+
typealias Input = RequestResponseMiddlewareBox<RequestConcludingAsyncReader, ResponseConcludingAsyncWriter>
2117
typealias NextInput = Never
2218

23-
init(
24-
requestConcludingAsyncReaderType: RequestConcludingAsyncReader.Type = RequestConcludingAsyncReader.self,
25-
responseConcludingAsyncWriterType: ResponseConcludingAsyncWriter.Type = ResponseConcludingAsyncWriter.self
26-
) {
27-
}
28-
2919
func intercept(
30-
input: Input,
31-
next: (NextInput) async throws -> Void
20+
input: consuming Input,
21+
next: (consuming NextInput) async throws -> Void
3222
) async throws {
33-
try await input.2(HTTPResponse(status: .accepted)).produceAndConclude { responseBodyAsyncWriter in
34-
var responseBodyAsyncWriter = responseBodyAsyncWriter
35-
_ = try await input.1.consumeAndConclude { bodyAsyncReader in
36-
var shouldContinue = true
37-
while shouldContinue {
38-
try await bodyAsyncReader.read { span in
39-
guard let span else {
40-
shouldContinue = false
41-
return
23+
try await input.withContents { request, requestReader, responseSender in
24+
var maybeReader = Optional(requestReader)
25+
try await responseSender.sendResponse(HTTPResponse(status: .accepted))
26+
.produceAndConclude { responseBodyAsyncWriter in
27+
var responseBodyAsyncWriter = responseBodyAsyncWriter
28+
if let reader = maybeReader.take() {
29+
_ = try await reader.consumeAndConclude { bodyAsyncReader in
30+
var shouldContinue = true
31+
var bodyAsyncReader = bodyAsyncReader
32+
while shouldContinue {
33+
try await bodyAsyncReader.read { span in
34+
guard let span else {
35+
shouldContinue = false
36+
return
37+
}
38+
try await responseBodyAsyncWriter.write(span)
39+
}
40+
}
4241
}
43-
try await responseBodyAsyncWriter.write(span)
42+
return HTTPFields(dictionaryLiteral: (HTTPField.Name.acceptEncoding, "encoding"))
43+
} else {
44+
fatalError("Closure run more than once")
4445
}
4546
}
46-
}
47-
return HTTPFields(dictionaryLiteral: (HTTPField.Name.acceptEncoding, "encoding"))
4847
}
4948
}
5049
}

0 commit comments

Comments
 (0)