Skip to content
Draft
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
12 changes: 4 additions & 8 deletions Examples/MiddlewareClient/ExampleMiddlewareClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,16 @@ import Middleware
@available(anyAppleOS 26.0, *)
struct ExampleMiddlewareClient<
Client: HTTPClient & ~Copyable,
OutWriter: CallerAsyncWriter & ~Copyable & SendableMetatype,
ClientMiddleware: Middleware & Sendable
>: HTTPClient, ~Copyable
where
OutWriter.WriteElement == UInt8,
OutWriter.FinalElement == HTTPFields?,
Client.Writer: SendableMetatype,
ClientMiddleware.Input: ~Copyable,
ClientMiddleware.NextInput: ~Copyable,
ClientMiddleware.Input == HTTPClientMiddlewareInput<OutWriter>,
ClientMiddleware.Input == HTTPClientMiddlewareInput<Client.Writer>,
ClientMiddleware.NextInput == HTTPClientMiddlewareInput<Client.Writer>
{
typealias RequestOptions = Client.RequestOptions
typealias Writer = OutWriter
typealias Writer = Client.Writer
typealias Reader = Client.Reader

var defaultRequestOptions: Client.RequestOptions {
Expand All @@ -54,9 +50,9 @@ where

mutating func perform<Return: ~Copyable>(
request: HTTPRequest,
body: consuming HTTPClientRequestBody<OutWriter>?,
body: consuming HTTPClientRequestBody<Writer>?,
options: RequestOptions,
responseHandler: (HTTPResponse, consuming Reader) async throws -> Return
responseHandler: (HTTPResponse, consuming Reader, consuming Future<Writer?>) async throws -> Return
) async throws -> Return {
return try await self.middleware.intercept(
input: HTTPClientMiddlewareInput(request: request, body: body)
Expand Down
3 changes: 2 additions & 1 deletion Examples/ProxyServer/ProxyServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ struct ProxyServer {
}!
// Pipe the server request body straight into the upstream writer.
try await reader.pipe(into: upstreamWriter)
return nil
}
) { response, upstreamReader in
) { response, upstreamReader, _ in
// Pipe the upstream client response body straight into the
// downstream response sender.
let writer = try await responseSender.take()!.send(response)
Expand Down
10 changes: 5 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ let extraSettings: [SwiftSetting] = [
let package = Package(
name: "HTTPAPIProposal",
platforms: [ // TODO: Needed until https://github.com/swiftlang/swift/issues/89028 is fixed
.macOS(.v15),
.iOS(.v18),
.watchOS(.v11),
.tvOS(.v18),
.visionOS(.v2),
.macOS(.v27),
.iOS(.v27),
.watchOS(.v27),
.tvOS(.v27),
.visionOS(.v27),
],
products: [
.library(name: "HTTPAPIs", targets: ["HTTPAPIs"]),
Expand Down
12 changes: 9 additions & 3 deletions Sources/AHCHTTPClient/AHC+HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient {
request: HTTPRequest,
body: consuming HTTPClientRequestBody<Writer>?,
options: RequestOptions,
responseHandler: (HTTPResponse, consuming Reader) async throws -> Return
responseHandler: (HTTPResponse, consuming Reader, consuming Future<Writer?>) async throws -> Return
) async throws -> Return {
guard let url = request.url else {
fatalError()
Expand All @@ -165,6 +165,10 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient {
var result: Result<Return, any Error>?
await withTaskGroup(of: Void.self) { taskGroup in

let pair = Promise.makePromise(of: Writer?.self)
var requestBodyPromise = Optional(pair.a)
let requestBodyFuture = pair.b

var ahcRequest = HTTPClientRequest(url: url.absoluteString)
ahcRequest.method = .init(rawValue: request.method.rawValue)
if !request.headerFields.isEmpty {
Expand All @@ -181,10 +185,12 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient {
for await ahcWriter in asyncStream {
do {
let writer = Writer(ahcWriter)
try await body.produce(into: writer)
let result = try await body.produce(into: writer)
requestBodyPromise.take()!.fulfill(result)
// writer.finish already calls requestBodyStreamFinished
break // the loop
} catch let error {
requestBodyPromise.take()!.fulfill(error: error)
// if we fail because the user throws in upload, we have to cancel the
// upload and fail the request I guess.
ahcWriter.fail(error)
Expand All @@ -211,7 +217,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient {
headerFields: responseFields
)

result = .success(try await responseHandler(response, Reader(body: ahcResponse.body)))
result = .success(try await responseHandler(response, Reader(body: ahcResponse.body), requestBodyFuture))
} catch {
result = .failure(error)
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ where
request: HTTPRequest,
body: consuming HTTPClientRequestBody<Writer>? = nil,
options: RequestOptions? = nil,
responseHandler: (HTTPResponse, consuming Reader) async throws -> Return,
responseHandler: (HTTPResponse, consuming Reader, consuming Future<Writer?>) async throws -> Return,
) async throws -> Return {
let options = options ?? self.defaultRequestOptions
return try await self.perform(request: request, body: body, options: options, responseHandler: responseHandler)
Expand Down Expand Up @@ -76,7 +76,7 @@ where
) async throws -> (response: HTTPResponse, bodyData: Data) {
let request = HTTPRequest(url: url, headerFields: headerFields)
let options = options ?? self.defaultRequestOptions
return try await self.perform(request: request, body: nil, options: options) { response, reader in
return try await self.perform(request: request, body: nil, options: options) { response, reader, _ in
(
response,
try await Self.collectBody(reader, upTo: limit)
Expand Down Expand Up @@ -108,7 +108,7 @@ where
) async throws -> (response: HTTPResponse, bodyData: Data) {
let request = HTTPRequest(method: .post, url: url, headerFields: headerFields)
let options = options ?? self.defaultRequestOptions
return try await self.perform(request: request, body: .data(bodyData), options: options) { response, reader in
return try await self.perform(request: request, body: .data(bodyData), options: options) { response, reader, _ in
(
response,
try await Self.collectBody(reader, upTo: limit)
Expand Down Expand Up @@ -140,7 +140,7 @@ where
) async throws -> (response: HTTPResponse, bodyData: Data) {
let request = HTTPRequest(method: .put, url: url, headerFields: headerFields)
let options = options ?? self.defaultRequestOptions
return try await self.perform(request: request, body: .data(bodyData), options: options) { response, reader in
return try await self.perform(request: request, body: .data(bodyData), options: options) { response, reader, _ in
(
response,
try await Self.collectBody(reader, upTo: limit)
Expand Down Expand Up @@ -172,7 +172,7 @@ where
) async throws -> (response: HTTPResponse, bodyData: Data) {
let request = HTTPRequest(method: .delete, url: url, headerFields: headerFields)
let options = options ?? self.defaultRequestOptions
return try await self.perform(request: request, body: bodyData.map { .data($0) }, options: options) { response, reader in
return try await self.perform(request: request, body: bodyData.map { .data($0) }, options: options) { response, reader, _ in
(
response,
try await Self.collectBody(reader, upTo: limit)
Expand Down Expand Up @@ -204,7 +204,7 @@ where
) async throws -> (response: HTTPResponse, bodyData: Data) {
let request = HTTPRequest(method: .patch, url: url, headerFields: headerFields)
let options = options ?? self.defaultRequestOptions
return try await self.perform(request: request, body: .data(bodyData), options: options) { response, reader in
return try await self.perform(request: request, body: .data(bodyData), options: options) { response, reader, _ in
(
response,
try await Self.collectBody(reader, upTo: limit)
Expand Down
2 changes: 1 addition & 1 deletion Sources/HTTPAPIs/Client/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@ public protocol HTTPClient<RequestOptions>: Sendable, ~Copyable, ~Escapable {
request: HTTPRequest,
body: consuming HTTPClientRequestBody<Writer>?,
options: RequestOptions,
responseHandler: (HTTPResponse, consuming Reader) async throws -> Return
responseHandler: (HTTPResponse, consuming Reader, consuming Future<Writer?>) async throws -> Return
) async throws -> Return
}
1 change: 1 addition & 0 deletions Sources/HTTPAPIs/Client/HTTPClientRequestBody+Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ extension HTTPClientRequestBody where Writer: ~Copyable {
copying: data.span.extracting(droppingFirst: Int(offset))
)
try await writer.finish(buffer: &buffer, finalElement: nil)
return nil
}
}
}
27 changes: 18 additions & 9 deletions Sources/HTTPAPIs/Client/HTTPClientRequestBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ where Writer.WriteElement == UInt8, Writer.FinalElement == HTTPFields? {
public let knownLength: Int64?

private enum WriteBody {
case restartable(@Sendable (consuming Writer) async throws -> Void)
case seekable(@Sendable (Int64, consuming Writer) async throws -> Void)
case restartable(@Sendable (consuming Writer) async throws -> Writer?)
case seekable(@Sendable (Int64, consuming Writer) async throws -> Writer?)
}
private let writeBody: WriteBody

/// Requests the body to be written into the writer.
/// - Parameters:
/// - writer: The destination into which to write the body.
/// - Throws: An error thrown from the body closure.
public func produce(into writer: consuming Writer) async throws {
public func produce(into writer: consuming Writer) async throws -> Writer? {
switch self.writeBody {
case .restartable(let writeBody):
try await writeBody(writer)
Expand All @@ -96,7 +96,7 @@ where Writer.WriteElement == UInt8, Writer.FinalElement == HTTPFields? {
/// - offset: The offset from which to start writing the body.
/// - writer: The destination into which to write the body.
/// - Throws: An error thrown from the body closure.
public func produce(offset: Int64, into writer: consuming Writer) async throws {
public func produce(offset: Int64, into writer: consuming Writer) async throws -> Writer? {
switch self.writeBody {
case .restartable:
fatalError("Request body is not seekable")
Expand All @@ -119,7 +119,7 @@ where Writer.WriteElement == UInt8, Writer.FinalElement == HTTPFields? {
/// ``CallerAsyncWriter/finish(buffer:finalElement:)`` to terminate the body.
public static func restartable(
knownLength: Int64? = nil,
_ body: @escaping @Sendable (consuming Writer) async throws -> Void
_ body: @escaping @Sendable (consuming Writer) async throws -> Writer?
) -> Self {
Self.init(
knownLength: knownLength,
Expand All @@ -141,7 +141,7 @@ where Writer.WriteElement == UInt8, Writer.FinalElement == HTTPFields? {
/// ``CallerAsyncWriter/finish(buffer:finalElement:)`` to terminate the body.
public static func seekable(
knownLength: Int64? = nil,
_ body: @escaping @Sendable (Int64, consuming Writer) async throws -> Void
_ body: @escaping @Sendable (Int64, consuming Writer) async throws -> Writer?
) -> Self {
Self.init(
knownLength: knownLength,
Expand All @@ -158,18 +158,27 @@ where Writer.WriteElement == UInt8, Writer.FinalElement == HTTPFields? {
// modules to map bodies.
package init<OtherWriter: ~Copyable & SendableMetatype>(
other: HTTPClientRequestBody<OtherWriter>,
transform: @escaping @Sendable (consuming Writer) -> OtherWriter
transform: @escaping @Sendable (consuming Writer) -> OtherWriter,
reverseTransform: @escaping @Sendable (consuming OtherWriter) -> Writer
) where Writer: SendableMetatype {
self.knownLength = other.knownLength
self.writeBody =
switch other.writeBody {
case .restartable(let writeBody):
.restartable { writer in
try await writeBody(transform(writer))
if let writer = try await writeBody(transform(writer)) {
reverseTransform(writer)
} else {
nil
}
}
case .seekable(let writeBody):
.seekable { offset, writer in
try await writeBody(offset, transform(writer))
if let writer = try await writeBody(offset, transform(writer)) {
reverseTransform(writer)
} else {
nil
}
}
}
}
Expand Down
Loading
Loading