Skip to content
Merged
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
9 changes: 3 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@ jobs:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_5_10_enabled: false
linux_6_0_enabled: false
linux_6_1_enabled: false
linux_6_2_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_3_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_enabled: false
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
windows_6_0_enabled: false
windows_6_1_enabled: false
Expand All @@ -33,6 +28,7 @@ jobs:
xcode_16_4_enabled: false
xcode_26_0_enabled: false
xcode_26_1_enabled: false
xcode_26_2_enabled: false
macos_xcode_build_enabled: false
ios_xcode_build_enabled: false
watchos_xcode_build_enabled: false
Expand All @@ -45,6 +41,7 @@ jobs:
linux_5_10_enabled: false
linux_6_0_enabled: false
linux_6_1_enabled: false
linux_nightly_next_enabled: false
windows_6_0_enabled: false
windows_6_1_enabled: false
windows_nightly_next_enabled: false
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,15 @@ jobs:
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
with:
license_header_check_project_name: "Swift HTTP API Proposal"
api_breakage_check_container_image: "swiftlang/swift:nightly-main-noble"
format_check_container_image: "swiftlang/swift:nightly-main-noble" # Needed due to https://github.com/swiftlang/swift-format/issues/1081

unit-tests:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_6_2_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_6_3_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_next_enabled: false
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
windows_6_0_enabled: false
windows_6_1_enabled: false
windows_nightly_6_1_enabled: false
windows_nightly_main_enabled: false

Expand All @@ -35,6 +32,7 @@ jobs:
xcode_16_4_enabled: false
xcode_26_0_enabled: false
xcode_26_1_enabled: false
xcode_26_2_enabled: false
macos_xcode_build_enabled: false
ios_xcode_build_enabled: false
watchos_xcode_build_enabled: false
Expand All @@ -47,6 +45,7 @@ jobs:
linux_5_10_enabled: false
linux_6_0_enabled: false
linux_6_1_enabled: false
linux_nightly_next_enabled: false
windows_6_0_enabled: false
windows_6_1_enabled: false
windows_nightly_next_enabled: false
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ target/
Package.resolved
/.vscode/
/.vstags
Package.resolved
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// swift-tools-version: 6.2
// swift-tools-version: 6.4

import PackageDescription

let extraSettings: [SwiftSetting] = [
.strictMemorySafety(),
.enableExperimentalFeature("SuppressedAssociatedTypes"),
.enableExperimentalFeature("SuppressedAssociatedTypesWithDefaults"),
.enableExperimentalFeature("LifetimeDependence"),
.enableExperimentalFeature("Lifetimes"),
.enableUpcomingFeature("LifetimeDependence"),
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ concurrency—to provide modern, safe, and efficient HTTP abstractions.
final—we're iterating on designs based on feedback and real-world usage. The
APIs and structure will continue to evolve as we refine the approach.

> [!IMPORTANT]
> This repository requires a Swift 6.4 aligned toolchain.

## Motivation

The Swift ecosystem currently lacks standardized, modern, and cross-platform
Expand Down
3 changes: 3 additions & 0 deletions Sources/AsyncStreaming/EitherError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ public enum EitherError<First: Error, Second: Error>: Error {
}
}
}

extension EitherError: Equatable where First: Equatable, Second: Equatable {}
extension EitherError: Hashable where First: Hashable, Second: Hashable {}
17 changes: 6 additions & 11 deletions Sources/AsyncStreaming/Reader/AsyncReader+forEach.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,22 @@ extension AsyncReader where Self: ~Copyable, Self: ~Escapable {
/// }
/// ```
@inlinable
public consuming func forEach<Failure: Error>(
body: (consuming Span<ReadElement>) async throws(Failure) -> Void
) async throws(Failure) where ReadFailure == Never {
public consuming func forEach(
body: (consuming Span<ReadElement>) async -> Void
) async where ReadFailure == Never {
var shouldContinue = true
while shouldContinue {
do {
try await self.read(maximumCount: nil) { (next) throws(Failure) -> Void in
try await self.read(maximumCount: nil) { (next) -> Void in
guard next.count > 0 else {
shouldContinue = false
return
}

try await body(next)
await body(next)
}
} catch {
switch error {
case .first:
fatalError()
case .second(let error):
throw error
}
fatalError()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//===----------------------------------------------------------------------===//

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension ConcludingAsyncReader where Self: ~Copyable {
extension ConcludingAsyncReader where Self: ~Copyable, Underlying: ~Copyable {
/// Collects elements from the underlying async reader and returns both the processed result and final element.
///
/// This method provides a convenient way to collect elements from the underlying reader while
Expand Down
4 changes: 2 additions & 2 deletions Sources/AsyncStreaming/Writer/ConcludingAsyncWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public protocol ConcludingAsyncWriter<Underlying, FinalElement>: ~Copyable, ~Esc
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension ConcludingAsyncWriter where Self: ~Copyable {
extension ConcludingAsyncWriter where Self: ~Copyable, Underlying: ~Copyable {
/// Produces a final element using the underlying async writer without returning a separate value.
///
/// This is a convenience method for cases where you only need to produce a final element
Expand Down Expand Up @@ -80,7 +80,7 @@ extension ConcludingAsyncWriter where Self: ~Copyable {
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension ConcludingAsyncWriter where Self: ~Copyable {
extension ConcludingAsyncWriter where Self: ~Copyable, Underlying: ~Copyable {
/// Writes a single element to the underlying writer and concludes with a final element.
///
/// This is a convenience method for simple scenarios where you need to write exactly one
Expand Down
10 changes: 8 additions & 2 deletions Sources/HTTPAPIs/Client/HTTPClient+Conveniences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ public import struct Foundation.Data
#endif

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension HTTPClient where Self: ~Copyable & ~Escapable {
extension HTTPClient
where
Self: ~Copyable & ~Escapable,
ResponseConcludingReader: ~Copyable,
ResponseConcludingReader.Underlying: ~Copyable,
RequestWriter: ~Copyable
{
/// Performs an HTTP request and processes the response.
///
/// This convenience method provides default values for `body` and `options` arguments,
Expand Down Expand Up @@ -223,7 +229,7 @@ extension HTTPClient where Self: ~Copyable & ~Escapable {
}

private static func collectBody<Reader: ConcludingAsyncReader>(_ body: consuming Reader, upTo limit: Int) async throws -> Data
where Reader: ~Copyable, Reader.Underlying.ReadElement == UInt8 {
where Reader: ~Copyable, Reader.Underlying: ~Copyable, Reader.Underlying.ReadElement == UInt8 {
try await body.collect(upTo: limit == .max ? .max : limit + 1) {
if $0.count > limit {
throw LengthLimitExceededError()
Expand Down
5 changes: 4 additions & 1 deletion Sources/HTTPAPIs/Client/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ public protocol HTTPClient<RequestOptions>: Sendable, ~Copyable, ~Escapable {
/// The type used to read response body data and trailers.
// TODO: Check if we should allow ~Escapable writers https://github.com/apple/swift-http-api-proposal/issues/13
associatedtype ResponseConcludingReader: ConcludingAsyncReader, ~Copyable, SendableMetatype
where ResponseConcludingReader.Underlying.ReadElement == UInt8, ResponseConcludingReader.FinalElement == HTTPFields?
where
ResponseConcludingReader.Underlying: ~Copyable,
ResponseConcludingReader.Underlying.ReadElement == UInt8,
ResponseConcludingReader.FinalElement == HTTPFields?

/// The default request options for `perform`.
var defaultRequestOptions: RequestOptions { get }
Expand Down
17 changes: 14 additions & 3 deletions Sources/HTTPAPIs/Server/HTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ public protocol HTTPServer<RequestConcludingReader, ResponseConcludingWriter>: S
/// The type used to read request body data and trailers.
// TODO: Check if we should allow ~Escapable readers https://github.com/apple/swift-http-api-proposal/issues/13
associatedtype RequestConcludingReader: ConcludingAsyncReader, ~Copyable, SendableMetatype
where RequestConcludingReader.Underlying.ReadElement == UInt8, RequestConcludingReader.FinalElement == HTTPFields?
where
RequestConcludingReader.Underlying: ~Copyable,
RequestConcludingReader.Underlying.ReadElement == UInt8,
RequestConcludingReader.FinalElement == HTTPFields?

/// The type used to write response body data and trailers.
// TODO: Check if we should allow ~Escapable writers https://github.com/apple/swift-http-api-proposal/issues/13
associatedtype ResponseConcludingWriter: ConcludingAsyncWriter, ~Copyable, SendableMetatype
where ResponseConcludingWriter.Underlying.WriteElement == UInt8, ResponseConcludingWriter.FinalElement == HTTPFields?
where
ResponseConcludingWriter.Underlying: ~Copyable,
ResponseConcludingWriter.Underlying.WriteElement == UInt8,
ResponseConcludingWriter.FinalElement == HTTPFields?

/// Starts an HTTP server with the specified request handler.
///
Expand All @@ -44,5 +50,10 @@ public protocol HTTPServer<RequestConcludingReader, ResponseConcludingWriter>: S
/// let server = // create an instance of a type conforming to the `ServerProtocol`
/// try await server.serve(handler: YourRequestHandler())
/// ```
func serve(handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>) async throws
func serve<Handler: HTTPServerRequestHandler>(handler: Handler) async throws
where
Handler.RequestReader == RequestConcludingReader,
Handler.RequestReader: ~Copyable,
Handler.ResponseWriter == ResponseConcludingWriter,
Handler.ResponseWriter: ~Copyable
}
12 changes: 11 additions & 1 deletion Sources/HTTPAPIs/Server/HTTPServerClosureRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public struct HTTPServerClosureRequestHandler<
ResponseWriter: ConcludingAsyncWriter & ~Copyable,
>: HTTPServerRequestHandler
where
RequestReader.Underlying: ~Copyable,
ResponseWriter.Underlying: ~Copyable,
RequestReader.Underlying.ReadElement == UInt8,
ResponseWriter.Underlying.WriteElement == UInt8,
RequestReader.FinalElement == HTTPFields?,
Expand Down Expand Up @@ -91,7 +93,15 @@ where
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension HTTPServer where Self: ~Copyable, Self: ~Escapable {
extension HTTPServer
where
Self: ~Copyable,
Self: ~Escapable,
RequestConcludingReader: ~Copyable,
RequestConcludingReader.Underlying: ~Copyable,
ResponseConcludingWriter: ~Copyable,
ResponseConcludingWriter.Underlying: ~Copyable
{
/// Starts an HTTP server with a closure-based request handler.
///
/// This method provides a convenient way to start an HTTP server using a closure to handle incoming requests.
Expand Down
4 changes: 2 additions & 2 deletions Sources/HTTPAPIs/Server/HTTPServerRequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@
public protocol HTTPServerRequestHandler<RequestReader, ResponseWriter>: Sendable {
/// The type used to read request body data and trailers.
associatedtype RequestReader: ConcludingAsyncReader, ~Copyable
where RequestReader.Underlying.ReadElement == UInt8, RequestReader.FinalElement == HTTPFields?
where RequestReader.Underlying: ~Copyable, RequestReader.Underlying.ReadElement == UInt8, RequestReader.FinalElement == HTTPFields?

/// The type used to write response body data and trailers.
associatedtype ResponseWriter: ConcludingAsyncWriter, ~Copyable
where ResponseWriter.Underlying.WriteElement == UInt8, ResponseWriter.FinalElement == HTTPFields?
where ResponseWriter.Underlying: ~Copyable, ResponseWriter.Underlying.WriteElement == UInt8, ResponseWriter.FinalElement == HTTPFields?

/// Handles an incoming HTTP request and generates a response.
///
Expand Down
2 changes: 1 addition & 1 deletion Sources/HTTPAPIs/Server/HTTPServerResponseSender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/// enforces proper HTTP semantics: exactly one non-informational response, followed by
/// optional response body streaming and trailers.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct HTTPResponseSender<ResponseWriter: ConcludingAsyncWriter & ~Copyable>: ~Copyable {
public struct HTTPResponseSender<ResponseWriter: ConcludingAsyncWriter & ~Copyable>: ~Copyable where ResponseWriter.Underlying: ~Copyable {
private let _sendInformational: (HTTPResponse) async throws -> Void
private let _send: (HTTPResponse) async throws -> ResponseWriter

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable
/// The type of errors that can occur during reading operations.
public typealias Failure = any Error

private var iterator: NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator?
private var iterator: Disconnected<NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator?>

internal var state: ReaderState

Expand All @@ -125,7 +125,7 @@ public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable
iterator: consuming sending NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator,
readerState: ReaderState
) {
self.iterator = iterator
self.iterator = .init(value: iterator)
self.state = readerState
}

Expand Down Expand Up @@ -157,7 +157,7 @@ public struct HTTPRequestConcludingAsyncReader: ConcludingAsyncReader, ~Copyable
public consuming func consumeAndConclude<Return, Failure: Error>(
body: nonisolated(nonsending) (consuming sending RequestBodyAsyncReader) async throws(Failure) -> Return
) async throws(Failure) -> (Return, HTTPFields?) {
if let iterator = self.iterator.sendingTake() {
if let iterator = self.iterator.take() {
let partsReader = RequestBodyAsyncReader(iterator: iterator, readerState: self.state)
let result = try await body(partsReader)
let trailers = self.state.wrapped.withLock { $0.trailers }
Expand All @@ -174,10 +174,27 @@ extension HTTPRequestConcludingAsyncReader: Sendable {}
@available(*, unavailable)
extension HTTPRequestConcludingAsyncReader.RequestBodyAsyncReader: Sendable {}

extension Optional {
mutating func sendingTake() -> sending Self {
let result = consume self
self = nil
return result
@usableFromInline
struct Disconnected<Value: ~Copyable>: ~Copyable, Sendable {
// This is safe since we take the value as sending and take consumes it
// and returns it as sending.
private nonisolated(unsafe) var value: Value?

@usableFromInline
init(value: consuming sending Value) {
unsafe self.value = .some(value)
}

@usableFromInline
consuming func take() -> sending Value {
nonisolated(unsafe) let value = unsafe self.value.take()!
return unsafe value
}

@usableFromInline
mutating func swap(newValue: consuming sending Value) -> sending Value {
nonisolated(unsafe) let value = unsafe self.value.take()!
unsafe self.value = consume newValue
return unsafe value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public import HTTPTypes
/// This forces structure in the response flow, requiring users to send a single response before they can stream a response body and
/// trailers using the returned `ResponseWriter`.
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
public struct HTTPResponseSender<ResponseWriter: ConcludingAsyncWriter & ~Copyable>: ~Copyable {
public struct HTTPResponseSender<ResponseWriter: ConcludingAsyncWriter & ~Copyable>: ~Copyable
where ResponseWriter.Underlying: ~Copyable & ~Escapable {
private let _sendInformational: (HTTPResponse) async throws -> Void
private let _send: (HTTPResponse) async throws -> ResponseWriter

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public protocol HTTPServer: Sendable, ~Copyable, ~Escapable {
/// `ReadElement`.
associatedtype RequestReader: ConcludingAsyncReader & ~Copyable & SendableMetatype
where
RequestReader.Underlying: ~Copyable,
RequestReader.Underlying.ReadElement == UInt8,
RequestReader.Underlying.ReadFailure == any Error,
RequestReader.FinalElement == HTTPFields?
Expand All @@ -34,6 +35,7 @@ public protocol HTTPServer: Sendable, ~Copyable, ~Escapable {
/// `WriteElement`.
associatedtype ResponseWriter: ConcludingAsyncWriter & ~Copyable & SendableMetatype
where
ResponseWriter.Underlying: ~Copyable,
ResponseWriter.Underlying.WriteElement == UInt8,
ResponseWriter.Underlying.WriteFailure == any Error,
ResponseWriter.FinalElement == HTTPFields?
Expand All @@ -55,5 +57,12 @@ public protocol HTTPServer: Sendable, ~Copyable, ~Escapable {
/// let server = // create an instance of a type conforming to the `HTTPServer` protocol
/// try await server.serve(handler: YourRequestHandler())
/// ```
func serve(handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>) async throws
func serve<RequestHandler: HTTPServerRequestHandler>(
handler: RequestHandler
) async throws
where
RequestHandler.RequestReader == RequestReader,
RequestHandler.ResponseWriter == ResponseWriter,
RequestHandler.RequestReader: ~Copyable,
RequestHandler.ResponseWriter: ~Copyable
}
Loading
Loading