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
18 changes: 5 additions & 13 deletions Examples/EchoServer/EchoServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,11 @@ struct EchoServer {
fatalError("Waiting for a concrete HTTP server implementation")
}

static func echo<Server: HTTPServer>(server: Server) async throws {
try await server.serve { request, requestContext, requestBodyAndTrailers, responseSender in
// Needed since we are lacking call-once closures
var requestBodyAndTrailers = Optional(requestBodyAndTrailers)
let responseBodyAndTrailers = try await responseSender.send(.init(status: .ok))

try await responseBodyAndTrailers.produceAndConclude { responseBody in
// Needed since we are lacking call-once closures
var responseBody = responseBody
return try await requestBodyAndTrailers.take()!.consumeAndConclude { reader in
try await responseBody.write(reader)
}
}
static func echo<Server: HTTPServer>(server: Server) async throws
where Server.Reader.Buffer == Server.ResponseSender.Writer.Buffer {
try await server.serve { request, requestContext, reader, responseSender in
let writer = try await responseSender.send(.init(status: .ok))
try await reader.pipe(into: writer)
}
}
}
2 changes: 1 addition & 1 deletion Examples/ExampleMiddleware/ForwardingMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public struct ForwardingMiddleware<Input: ~Copyable & ~Escapable>: Middleware {
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension Middleware {
extension Middleware where Input: ~Copyable & ~Escapable, NextInput: ~Copyable & ~Escapable {
public func forwarding() -> ForwardingMiddleware<Input> {
ForwardingMiddleware()
}
Expand Down
35 changes: 35 additions & 0 deletions Examples/ExampleMiddleware/HTTPClientMiddlewareInput.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

public import HTTPAPIs

/// The input passed through client-side middleware: the request head plus the
/// request body the user wants to send.
///
/// Mirrors ``HTTPServerMiddlewareInput`` on the server side. Wrapping
/// middlewares can substitute a different `Writer` type for `NextInput` so
/// the inner stage sees a wrapped body that intercepts the bytes the user
/// wrote.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct HTTPClientMiddlewareInput<Writer: HTTPBodyWriter & ~Copyable & SendableMetatype>: ~Copyable {
public var request: HTTPRequest
public var body: HTTPClientRequestBody<Writer>?

public init(request: HTTPRequest, body: consuming HTTPClientRequestBody<Writer>?) {
self.request = request
self.body = body
}
}

@available(*, unavailable)
extension HTTPClientMiddlewareInput: Sendable {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import BasicContainers
public import HTTPAPIs
public import Middleware

/// A client-side middleware that observes all request body bytes and appends
/// a checksum (XOR of all bytes) as the `X-Body-Checksum` trailer.
///
/// Client-side mirror of ``HTTPServerResponseChecksumTrailerMiddleware``. The
/// `body` field of the input is wrapped with a ``ChecksumRequestWriter`` so
/// the inner stage (eventually the underlying client) receives a body whose
/// writes are intercepted to update the checksum, and whose `finish` appends
/// the `X-Body-Checksum` trailer.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct HTTPClientRequestChecksumTrailerMiddleware<
Writer: HTTPBodyWriter & ~Copyable & SendableMetatype
>: Middleware, Sendable {
public typealias Input = HTTPClientMiddlewareInput<ChecksumRequestWriter<Writer>>
public typealias NextInput = HTTPClientMiddlewareInput<Writer>

public init(writerType: Writer.Type = Writer.self) {}

public func intercept<Return: ~Copyable>(
input: consuming Input,
next: (consuming NextInput) async throws -> Return
) async throws -> Return {
let translatedBody: HTTPClientRequestBody<Writer>? = input.body.map { userBody in
HTTPClientRequestBody<Writer>(other: userBody) { baseWriter in
ChecksumRequestWriter(wrapping: baseWriter)
}
}
return try await next(
HTTPClientMiddlewareInput(request: input.request, body: translatedBody)
)
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension Middleware where Input: ~Copyable & ~Escapable, NextInput: ~Copyable & ~Escapable {
/// Adds a middleware that emits an `X-Body-Checksum` trailer covering the request body.
public func checksumTrailer<Writer>()
-> HTTPClientRequestChecksumTrailerMiddleware<Writer>
where
Input == HTTPClientMiddlewareInput<Writer>,
Writer: HTTPBodyWriter & ~Copyable & SendableMetatype
{
HTTPClientRequestChecksumTrailerMiddleware()
}
}

/// A wrapping ``HTTPBodyWriter`` that XORs every written byte into a running
/// checksum and emits an `X-Body-Checksum` trailer at conclude time.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct ChecksumRequestWriter<Base: HTTPBodyWriter & ~Copyable>: HTTPBodyWriter, ~Copyable, SendableMetatype
where Base: SendableMetatype {
public typealias WriteElement = UInt8
public typealias WriteFailure = Base.WriteFailure
public typealias Buffer = Base.Buffer

@usableFromInline
var underlying: Base
@usableFromInline
var checksum: UInt8

init(wrapping writer: consuming Base) {
self.underlying = writer
self.checksum = 0
}

public mutating func write<Return: ~Copyable, Failure: Error>(
_ body: (inout Buffer) async throws(Failure) -> Return
) async throws(EitherError<Base.WriteFailure, Failure>) -> Return {
try await self.underlying.write { buffer throws(Failure) in
let result = try await body(&buffer)
buffer._borrowingForEach {
self.checksum ^= $0
}
return result
}
}

public consuming func finish<Failure: Error>(
body: (inout Buffer) async throws(Failure) -> HTTPFields?
) async throws(EitherError<Base.WriteFailure, Failure>) {
// Move state out of self before the consuming call. Capturing
// `self.checksum` directly while consuming `self.underlying` triggers
// a use-after-consume on `self`.
var checksum = self.checksum
try await self.underlying.finish { buffer throws(Failure) in
var trailers = try await body(&buffer) ?? .init()
buffer._borrowingForEach {
checksum ^= $0
}
trailers.append(.init(name: .init("X-Body-Checksum")!, value: String(checksum, radix: 16)))
return trailers
}
}
}
Loading
Loading